diff --git a/package.json b/package.json index 6de5176b..9a22900a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.5-dev.5abbcf8", + "version": "2.5.0-beta.6-dev.9a4c12f", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", diff --git a/samples/using-typescript/skyflow-elements-update/src/index.ts b/samples/using-typescript/skyflow-elements-update/src/index.ts index 9168aeb9..f50d4460 100644 --- a/samples/using-typescript/skyflow-elements-update/src/index.ts +++ b/samples/using-typescript/skyflow-elements-update/src/index.ts @@ -2,7 +2,7 @@ Copyright (c) 2025 Skyflow, Inc. */ import Skyflow, { - CollectContainer, + CollectContainer, CollectElement, CollectElementInput, CollectElementOptions, @@ -48,11 +48,13 @@ try { logLevel: Skyflow.LogLevel.ERROR, env: Skyflow.Env.PROD, }, - } + }; const skyflowClient: Skyflow = Skyflow.init(config); // Create collect Container. - const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT) as CollectContainer; + const collectContainer = skyflowClient.container( + Skyflow.ContainerType.COLLECT + ) as CollectContainer; // Custom styles for collect elements. const collectStylesOptions = { @@ -128,7 +130,7 @@ try { label: "Cvv", placeholder: "cvv", type: Skyflow.ElementType.CVV, - } + }; const cvvElement: CollectElement = collectContainer.create(cvvInput); const expiryDateInput: CollectElementInput = { @@ -138,8 +140,9 @@ try { label: "Expiry Date", placeholder: "MM/YYYY", type: Skyflow.ElementType.EXPIRATION_DATE, - } - const expiryDateElement: CollectElement = collectContainer.create(expiryDateInput); + }; + const expiryDateElement: CollectElement = + collectContainer.create(expiryDateInput); const cardholderNameInput: CollectElementInput = { table: "pii_fields", @@ -148,8 +151,9 @@ try { label: "Card Holder Name", placeholder: "cardholder name", type: Skyflow.ElementType.CARDHOLDER_NAME, - } - const cardHolderNameElement: CollectElement = collectContainer.create(cardholderNameInput); + }; + const cardHolderNameElement: CollectElement = + collectContainer.create(cardholderNameInput); // Mount the elements. cardNumberElement.mount("#collectCardNumber"); @@ -184,11 +188,15 @@ try { cardNumberElement.on(Skyflow.EventName.CHANGE, (state: ElementState) => { if (state.isValid) { // update cvv element validation rule. - if (findCvvLength((state.value) as string) === 3) { - const updateOptions: CollectElementUpdateOptions = { validations: [length3Rule] }; + if (findCvvLength(state.value as string) === 3) { + const updateOptions: CollectElementUpdateOptions = { + validations: [length3Rule], + }; cvvElement.update(updateOptions); } else { - const updateOptions: CollectElementUpdateOptions = { validations: [length4Rule] }; + const updateOptions: CollectElementUpdateOptions = { + validations: [length4Rule], + }; cvvElement.update(updateOptions); } } @@ -225,175 +233,194 @@ try { } // Collect all elements data. - const collectButton = document.getElementById("collectPCIData") as HTMLButtonElement; + const collectButton = document.getElementById( + "collectPCIData" + ) as HTMLButtonElement; if (collectButton) { collectButton.addEventListener("click", () => { - const collectResponse: Promise = collectContainer.collect(); + const collectResponse: Promise = + collectContainer.collect(); collectResponse .then((response: CollectResponse) => { console.log(response); collectResponseData = response; - const responseElement = document.getElementById('collectResponse') as HTMLElement; + const responseElement = document.getElementById( + "collectResponse" + ) as HTMLElement; if (responseElement) { responseElement.innerHTML = JSON.stringify(response, null, 2); } - }) - .catch((err: CollectResponse) => { - const errorElement = document.getElementById('collectResponse') as HTMLElement; - if (errorElement){ - errorElement.innerHTML = JSON.stringify(err, null, 2); - } - console.log(err); - }); - }); - } - revealView!.style.visibility = "visible"; + revealView.style.visibility = "visible"; - const revealStyleOptions = { - inputStyles: { - base: { - border: "1px solid #eae8ee", - padding: "10px 16px", - borderRadius: "4px", - color: "#1d1d1d", - marginTop: "4px", - fontFamily: '"Roboto", sans-serif', - }, - global: { - "@import": - 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - }, - } as InputStyles, - labelStyles: { - base: { - fontSize: "16px", - fontWeight: "bold", - fontFamily: '"Roboto", sans-serif', - }, - global: { - "@import": - 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - }, - } as LabelStyles, - errorTextStyles: { - base: { - color: "#f44336", - paddingLeft: "20px", - fontFamily: '"Roboto", sans-serif', - }, - global: { - "@import": - 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - }, - } as ErrorTextStyles, - }; + const revealStyleOptions = { + inputStyles: { + base: { + border: "1px solid #eae8ee", + padding: "10px 16px", + borderRadius: "4px", + color: "#1d1d1d", + marginTop: "4px", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as InputStyles, + labelStyles: { + base: { + fontSize: "16px", + fontWeight: "bold", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: "#f44336", + paddingLeft: "20px", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as ErrorTextStyles, + }; - // Create Reveal Elements With Tokens. - const fieldsTokenData = collectResponseData.records![0].fields; - const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL) as RevealContainer; - - const revealCardNumberInput: RevealElementInput = { - token: fieldsTokenData.card_number, - label: "Card Number", - ...revealStyleOptions, - }; - const revealCardNumberElement: RevealElement = revealContainer.create(revealCardNumberInput); - revealCardNumberElement.mount("#revealCardNumber"); + // Create Reveal Elements With Tokens. + const fieldsTokenData = collectResponseData.records![0].fields; + const revealContainer = skyflowClient.container( + Skyflow.ContainerType.REVEAL + ) as RevealContainer; - const revealCardCvvInput: RevealElementInput = { - token: fieldsTokenData.cvv, - label: "CVV", - ...revealStyleOptions, - altText: "###", - }; - const revealCardCvvElement: RevealElement = revealContainer.create(revealCardCvvInput); - revealCardCvvElement.mount("#revealCvv"); + const revealCardNumberInput: RevealElementInput = { + token: fieldsTokenData.card_number, + label: "Card Number", + ...revealStyleOptions, + }; + const revealCardNumberElement: RevealElement = revealContainer.create( + revealCardNumberInput + ); + revealCardNumberElement.mount("#revealCardNumber"); - const revealCardExpiryInput: RevealElementInput = { - token: fieldsTokenData.expiration_date, - label: "Card Expiry Date", - ...revealStyleOptions, - }; - const revealCardExpiryElement: RevealElement = revealContainer.create(revealCardExpiryInput); - revealCardExpiryElement.mount("#revealExpiryDate"); + const revealCardCvvInput: RevealElementInput = { + token: fieldsTokenData.cvv, + label: "CVV", + ...revealStyleOptions, + altText: "###", + }; + const revealCardCvvElement: RevealElement = + revealContainer.create(revealCardCvvInput); + revealCardCvvElement.mount("#revealCvv"); - const revealCardholderNameInput: RevealElementInput = { - token: fieldsTokenData.name, - label: "Card Holder Name", - ...revealStyleOptions, - }; - const revealCardholderNameElement: RevealElement = revealContainer.create(revealCardholderNameInput); - revealCardholderNameElement.mount("#revealCardholderName"); + const revealCardExpiryInput: RevealElementInput = { + token: fieldsTokenData.expiration_date, + label: "Card Expiry Date", + ...revealStyleOptions, + }; + const revealCardExpiryElement: RevealElement = revealContainer.create( + revealCardExpiryInput + ); + revealCardExpiryElement.mount("#revealExpiryDate"); - const revealButton = document.getElementById("revealPCIData") as HTMLButtonElement; + const revealCardholderNameInput: RevealElementInput = { + token: fieldsTokenData.name, + label: "Card Holder Name", + ...revealStyleOptions, + }; + const revealCardholderNameElement: RevealElement = + revealContainer.create(revealCardholderNameInput); + revealCardholderNameElement.mount("#revealCardholderName"); - // update Reveal elements' properties - const updateRevealElementsButton = document.getElementById( - "updateRevealElements" - ) as HTMLButtonElement; - if (updateRevealElementsButton) { - updateRevealElementsButton.addEventListener("click", () => { - // update label,inputStyles on cardholderName, - revealCardholderNameElement.update({ - label: "CARDHOLDER NAME", - inputStyles: { - base: { - color: "#aa11aa", - }, - }, - } as RevealElementInput); + const revealButton = document.getElementById( + "revealPCIData" + ) as HTMLButtonElement; - // update label,labelSyles on card number - revealCardNumberElement.update({ - label: "CARD NUMBER", - labelStyles: { - base: { - borderWidth: "5px", - }, - }, - } as RevealElementInput); + // update Reveal elements' properties + const updateRevealElementsButton = document.getElementById( + "updateRevealElements" + ) as HTMLButtonElement; + if (updateRevealElementsButton) { + updateRevealElementsButton.addEventListener("click", () => { + // update label,inputStyles on cardholderName, + revealCardholderNameElement.update({ + label: "CARDHOLDER NAME", + inputStyles: { + base: { + color: "#aa11aa", + }, + }, + } as RevealElementInput); - // update redaction,inputStyles on expiry date - revealCardExpiryElement.update({ - redaction: Skyflow.RedactionType.REDACTED, - inputStyles: { - base: { - backgroundColor: "#000", - color: "#fff", - }, - }, - } as RevealElementInput); + // update label,labelSyles on card number + revealCardNumberElement.update({ + label: "CARD NUMBER", + labelStyles: { + base: { + borderWidth: "5px", + }, + }, + } as RevealElementInput); - // update altText,token,inputStyles,errorTextStyles on cvv - revealCardCvvElement.update({ - altText: "XXXX-XX", - token: "new-random-roken", - inputStyles: { - base: { - color: "#fff", - backgroundColor: "#000", - borderColor: "#f00", - borderWidth: "5px", - }, - }, - errorTextStyles: { - base: { - backgroundColor: "#000", - border: "1px #f00 solid", - }, - }, - } as RevealElementInput); - }); - } + // update redaction,inputStyles on expiry date + revealCardExpiryElement.update({ + redaction: Skyflow.RedactionType.REDACTED, + inputStyles: { + base: { + backgroundColor: "#000", + color: "#fff", + }, + }, + } as RevealElementInput); - if (revealButton) { - revealButton.addEventListener("click", () => { - const revealResponse: Promise = revealContainer.reveal(); - revealResponse.then((res: RevealResponse) => { - console.log(res); + // update altText,token,inputStyles,errorTextStyles on cvv + revealCardCvvElement.update({ + altText: "XXXX-XX", + token: "new-random-roken", + inputStyles: { + base: { + color: "#fff", + backgroundColor: "#000", + borderColor: "#f00", + borderWidth: "5px", + }, + }, + errorTextStyles: { + base: { + backgroundColor: "#000", + border: "1px #f00 solid", + }, + }, + } as RevealElementInput); + }); + } + + if (revealButton) { + revealButton.addEventListener("click", () => { + const revealResponse: Promise = + revealContainer.reveal(); + revealResponse + .then((res: RevealResponse) => { + console.log(res); + }) + .catch((err: RevealResponse) => { + console.log(err); + }); + }); + } }) - .catch((err: RevealResponse) => { + .catch((err: CollectResponse) => { + const errorElement = document.getElementById( + "collectResponse" + ) as HTMLElement; + if (errorElement) { + errorElement.innerHTML = JSON.stringify(err, null, 2); + } console.log(err); }); }); diff --git a/src/client/index.ts b/src/client/index.ts index 3f51f25c..c13e5a32 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -10,9 +10,10 @@ import sdkDetails from '../../package.json'; import { getMetaObject, } from '../utils/helpers'; +import { ClientMetadata } from '../core/internal/internal-types'; export interface IClientRequest { - body?: any; + body?: Document | XMLHttpRequestBodyInit | null; headers?: Record; requestMethod: | 'GET' @@ -30,24 +31,30 @@ export interface SdkInfo { sdkName: string; sdkVersion: string; } + +export interface ClientToJSON { + config: ISkyflow; + metaData: ClientMetadata; +} + class Client { config: ISkyflow; - #metaData: any; + #metaData: ClientMetadata; - constructor(config: ISkyflow, metadata) { + constructor(config: ISkyflow, metadata: ClientMetadata) { this.config = config; this.#metaData = metadata; } - toJSON() { + toJSON(): ClientToJSON { return { config: this.config, metaData: this.#metaData, }; } - static fromJSON(json) { + static fromJSON(json: ClientToJSON) { return new Client(json.config, json.metaData); } @@ -63,7 +70,7 @@ class Client { if (request.headers) { const metaDataObject = getMetaObject(sdkDetails, this.#metaData, navigator); request.headers[SKY_METADATA_HEADER] = JSON.stringify(metaDataObject); - const { headers } = request; + const headers = request.headers; Object.keys(request.headers).forEach((key) => { if (!(key === 'content-type' && headers[key] && headers[key].includes(ContentType.FORMDATA))) { httpRequest.setRequestHeader(key, headers[key]); @@ -75,7 +82,11 @@ class Client { || request.headers?.['content-type']?.includes(ContentType.FORMDATA)) { httpRequest.send(request.body); } else { - httpRequest.send(JSON.stringify({ ...request.body })); + /* Earlier we were stringifying here, but due to TS, we're stringifying + at the point where we are creating the request. Since the body parameter + doesn't accept JSON object. + */ + httpRequest.send(request.body); } httpRequest.onload = () => { diff --git a/src/core-utils/collect.ts b/src/core-utils/collect.ts index e4672d8a..93181075 100644 --- a/src/core-utils/collect.ts +++ b/src/core-utils/collect.ts @@ -10,10 +10,12 @@ import { getAccessToken } from '../utils/bus-events'; import { IInsertRecordInput, IInsertRecord, IValidationRule, ValidationRuleType, MessageType, LogLevel, + InsertResponse, } from '../utils/common'; import SKYFLOW_ERROR_CODE from '../utils/constants'; import { printLog } from '../utils/logs-helper'; import IFrameFormElement from '../core/internal/iframe-form'; +import { BatchInsertRequestBody } from '../core/internal/internal-types'; export interface IUpsertOptions{ table: string, @@ -35,8 +37,8 @@ export const getUpsertColumn = (tableName: string, options:Array export const constructInsertRecordRequest = ( records: IInsertRecordInput, options: Record = { tokens: true }, -) => { - const requestBody: any = []; +): Array => { + const requestBody: Array = []; if (options?.tokens || options === null) { records.records.forEach((record, index) => { const upsertColumn = getUpsertColumn(record.table, options.upsert); @@ -74,7 +76,7 @@ export const constructInsertRecordResponse = ( responseBody: any, tokens: boolean, records: IInsertRecord[], -) => { +): InsertResponse => { if (tokens) { return { records: responseBody.responses @@ -207,12 +209,12 @@ const updateRecordsInVault = ( skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'table'); skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'skyflowID'); return client.request({ - body: { + body: JSON.stringify({ record: { fields: { ...skyflowIdRecord.fields }, }, tokenization: options?.tokens !== undefined ? options.tokens : true, - }, + }), requestMethod: 'PUT', url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${table}/${skyflowID}`, headers: { diff --git a/src/core-utils/delete.ts b/src/core-utils/delete.ts index 01f1c946..0b6a6a0f 100644 --- a/src/core-utils/delete.ts +++ b/src/core-utils/delete.ts @@ -2,7 +2,9 @@ import Client from '../client'; import SkyflowError from '../libs/skyflow-error'; import { getAccessToken } from '../utils/bus-events'; import { + IDeleteOptions, IDeleteRecord, + IDeleteRecordInput, IDeleteResponseType, LogLevel, MessageType, @@ -40,9 +42,9 @@ export const deleteRecordsFromVault = async ( }; export const deleteData = async ( - records, - options, - client, + records: IDeleteRecordInput, + options: IDeleteOptions, + client: Client, ): Promise => { const clientId = client.toJSON()?.metaData?.uuid || ''; diff --git a/src/core-utils/reveal.ts b/src/core-utils/reveal.ts index 7285716d..7e11fc37 100644 --- a/src/core-utils/reveal.ts +++ b/src/core-utils/reveal.ts @@ -11,6 +11,11 @@ import { IRenderResponseType, IGetOptions, RenderFileResponse, + RevealResponse, + GetResponse, + GetResponseRecord, + GetByIdResponse, + GetByIdResponseRecord, } from '../utils/common'; import { printLog } from '../utils/logs-helper'; import { FILE_DOWNLOAD_URL_PARAM } from '../core/constants'; @@ -52,7 +57,7 @@ const getRecordsFromVault = ( client: Client, authToken:string, options?: IGetOptions, -) => { +): Promise => { let paramList: string = ''; skyflowIdRecord.ids?.forEach((skyflowId) => { @@ -81,13 +86,13 @@ const getRecordsFromVault = ( authorization: `Bearer ${authToken}`, 'content-type': 'application/json', }, - }); + }) as Promise; }; const getSkyflowIdRecordsFromVault = ( skyflowIdRecord: ISkyflowIdRecord, client: Client, authToken:string, -) => { +): Promise => { let paramList: string = ''; skyflowIdRecord.ids.forEach((skyflowId) => { @@ -103,7 +108,7 @@ const getSkyflowIdRecordsFromVault = ( authorization: `Bearer ${authToken}`, 'content-type': 'application/json', }, - }); + }) as Promise; }; const getTokenRecordsFromVault = ( token:string, @@ -120,14 +125,14 @@ const getTokenRecordsFromVault = ( 'content-type': 'application/json', }, body: - { + JSON.stringify({ detokenizationParameters: [ { token, redaction, }, ], - }, + }), }); }; @@ -271,36 +276,43 @@ export const formatForRenderClient = (response: IRenderResponseType, column: str return formattedResponse; }; -export const formatRecordsForClient = (response: IRevealResponseType) => { +export const formatRecordsForClient = (response: IRevealResponseType): RevealResponse => { + const revealResponse: RevealResponse = {}; if (response.records) { const successRecords = response.records.map((record) => ({ token: record.token, valueType: record.valueType, })); - if (response.errors) return { success: successRecords, errors: response.errors }; - return { success: successRecords }; + revealResponse.success = successRecords; } - return { errors: response.errors }; + if (response.errors) { + const errorRecords = response.errors.map((errorRecord) => ({ + token: errorRecord.token, + error: errorRecord.error, + })); + revealResponse.errors = errorRecords; + } + return revealResponse; }; export const fetchRecordsGET = async ( skyflowIdRecords: IGetRecord[], client: Client, options?: IGetOptions, -) => new Promise((rootResolve, rootReject) => { +): Promise => new Promise((rootResolve, rootReject) => { let vaultResponseSet: Promise[]; const clientId = client.toJSON()?.metaData?.uuid || ''; getAccessToken(clientId).then((authToken) => { vaultResponseSet = skyflowIdRecords.map( - (skyflowIdRecord) => new Promise((resolve, reject) => { + (skyflowIdRecord: IGetRecord) => new Promise((resolve, reject) => { getRecordsFromVault(skyflowIdRecord, client, authToken as string, options) .then( - (resolvedResult: any) => { - const response: any[] = []; - const recordsData: any[] = resolvedResult.records; + (resolvedResult: GetResponse) => { + const response: GetResponseRecord[] = []; + const recordsData: GetResponseRecord[] = resolvedResult.records || []; recordsData.forEach((fieldData) => { const id = fieldData.fields.skyflow_id; - const currentRecord = { + const currentRecord: GetResponseRecord = { fields: { id, ...fieldData.fields, @@ -329,7 +341,7 @@ export const fetchRecordsGET = async ( reject(errorResponse); }, ) - .catch((error) => { + .catch((error: unknown) => { reject(error); }); }), @@ -358,20 +370,20 @@ export const fetchRecordsGET = async ( export const fetchRecordsBySkyflowID = async ( skyflowIdRecords: ISkyflowIdRecord[], client: Client, -) => new Promise((rootResolve, rootReject) => { +): Promise => new Promise((rootResolve, rootReject) => { let vaultResponseSet: Promise[]; const clientId = client.toJSON()?.metaData?.uuid || ''; getAccessToken(clientId).then((authToken) => { vaultResponseSet = skyflowIdRecords.map( - (skyflowIdRecord) => new Promise((resolve, reject) => { + (skyflowIdRecord: ISkyflowIdRecord) => new Promise((resolve, reject) => { getSkyflowIdRecordsFromVault(skyflowIdRecord, client, authToken as string) .then( - (resolvedResult: any) => { + (resolvedResult: GetByIdResponse) => { const response: any[] = []; - const recordsData: any[] = resolvedResult.records; + const recordsData: GetByIdResponseRecord[] = resolvedResult.records || []; recordsData.forEach((fieldData) => { const id = fieldData.fields.skyflow_id; - const currentRecord = { + const currentRecord: GetByIdResponseRecord = { fields: { id, ...fieldData.fields, diff --git a/src/core/external/collect/collect-container.ts b/src/core/external/collect/collect-container.ts index 285f4bc6..2642d236 100644 --- a/src/core/external/collect/collect-container.ts +++ b/src/core/external/collect/collect-container.ts @@ -17,6 +17,7 @@ import { CollectResponse, ICollectOptions, UploadFilesResponse, + ContainerOptions, } from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import logs from '../../../utils/logs'; @@ -32,11 +33,40 @@ import { CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, FRAME_ELEMENT, COLLECT_TYPES, + ElementType, } from '../../constants'; import Container from '../common/container'; import CollectElement from './collect-element'; import EventEmitter from '../../../event-emitter'; import properties from '../../../properties'; +import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; + +export interface ICollectElement { + elementType: ElementType; + elementName: string; + name: string; + table?: string; + column?: string; + sensitive?: boolean; + replacePattern?: RegExp; + mask?: string[]; + value?: string; + isMounted: boolean; + [key: string]: unknown; +} + +export interface ElementGroupItem extends CollectElementInput, CollectElementOptions { + elementType: ElementType; + name?: string; + accept?: string[]; + elementName?: string; +} + +export interface ElementGroup { + rows: Array<{ + elements: Array; + }>; +} const CLASS_NAME = 'CollectContainer'; class CollectContainer extends Container { @@ -44,11 +74,11 @@ class CollectContainer extends Container { #elements: Record = {}; - #metaData: any; + #metaData: Metadata; - #context:Context; + #context: Context; - #skyflowElements:any; + #skyflowElements: Array; type:string = ContainerType.COLLECT; @@ -58,7 +88,12 @@ class CollectContainer extends Container { #isSkyflowFrameReady: boolean = false; - constructor(options, metaData, skyflowElements, context) { + constructor( + metaData: Metadata, + skyflowElements: Array, + context: Context, + options?: ContainerOptions, + ) { super(); this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; this.#containerId = uuid(); @@ -97,35 +132,34 @@ class CollectContainer extends Container { create = (input: CollectElementInput, options: CollectElementOptions = { required: false, - }) => { + }): CollectElement => { validateCollectElementInput(input, this.#context.logLevel); const validations = formatValidations(input.validations); const formattedOptions = formatOptions(input.type, options, this.#context.logLevel); - const elementGroup = { - rows: [ - { - elements: [ - { - elementType: input.type, - name: input.column, - accept: options.allowedFileType, - ...input, - ...formattedOptions, - validations, - }, - ], - }, - ], + + const elementGroup: ElementGroup = { + rows: [{ + elements: [{ + elementType: input.type, + name: input.column, + accept: options.allowedFileType, + ...input, + ...formattedOptions, + validations, + }], + }], }; + return this.#createMultipleElement(elementGroup, true); }; #createMultipleElement = ( - multipleElements: any, + multipleElements: ElementGroup, isSingleElementAPI: boolean = false, - ) => { + ): CollectElement => { const elements: any[] = []; const tempElements = deepClone(multipleElements); + tempElements.rows.forEach((row) => { row.elements.forEach((element) => { const options = element; @@ -362,7 +396,7 @@ class CollectContainer extends Container { }); }; - uploadFiles = (options: ICollectOptions) :Promise => { + uploadFiles = (options?: ICollectOptions): Promise => { this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; if (this.#isSkyflowFrameReady) { return new Promise((resolve, reject) => { @@ -472,7 +506,7 @@ class CollectContainer extends Container { if (!mountedIframeIds.length) return; this.#removeUnmountedElements(mountedIframeIds); - } catch (error) { + } catch (error: unknown) { printLog(`${error}`, MessageType.LOG, this.#context.logLevel); } }; diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 298aa5d2..8dfc05de 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -41,6 +41,7 @@ import { pushElementEventWithTimeout, updateMetricObjectValue, } from '../../../metrics'; +import { Metadata, ContainerProps, InternalState } from '../../internal/internal-types'; const CLASS_NAME = 'Element'; class CollectElement extends SkyflowElement { @@ -54,7 +55,7 @@ class CollectElement extends SkyflowElement { #isSingleElementAPI: boolean = false; - #states: any[]; + #states: InternalState[]; #elements: any[]; @@ -72,7 +73,7 @@ class CollectElement extends SkyflowElement { #group: any; - #metaData: any; + #metaData: Metadata; #eventEmitter: EventEmitter = new EventEmitter(); @@ -97,8 +98,8 @@ class CollectElement extends SkyflowElement { constructor( elementId: string, elementGroup: any, - metaData: any, - container: any, + metaData: Metadata, + container: ContainerProps, isSingleElementAPI: boolean = false, destroyCallback: Function, updateCallback: Function, @@ -318,7 +319,7 @@ class CollectElement extends SkyflowElement { } }; - #onUpdate = (callback) => { + #onUpdate = (callback: Function) => { // todo: us bus if else there will be an infinite loop if (!this.#isSingleElementAPI) { this.#eventEmitter.on( @@ -331,7 +332,7 @@ class CollectElement extends SkyflowElement { } }; - updateElement = (elementOptions) => { + updateElement = (elementOptions: { elementName: string } & CollectElementUpdateOptions) => { this.#bus.emit(ELEMENT_EVENTS_TO_IFRAME.SET_VALUE + elementOptions.elementName, { name: elementOptions.elementName, options: elementOptions, @@ -446,7 +447,7 @@ class CollectElement extends SkyflowElement { } } - #onDestroy = (callback) => { + #onDestroy = (callback: Function) => { this.#eventEmitter.on( ELEMENT_EVENTS_TO_IFRAME.DESTROY_FRAME, () => { diff --git a/src/core/external/collect/compose-collect-container.ts b/src/core/external/collect/compose-collect-container.ts index 4a9f0480..c3f7d0ef 100644 --- a/src/core/external/collect/compose-collect-container.ts +++ b/src/core/external/collect/compose-collect-container.ts @@ -21,6 +21,9 @@ import { CollectElementOptions, ICollectOptions, CollectResponse, + InputStyles, + ErrorTextStyles, + ContainerOptions, } from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import logs from '../../../utils/logs'; @@ -38,6 +41,13 @@ import { import Container from '../common/container'; import CollectElement from './collect-element'; import ComposableElement from './compose-collect-element'; +import { ElementGroup, ElementGroupItem } from './collect-container'; +import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; + +export interface ComposableElementGroup extends ElementGroup { + styles: InputStyles; + errorTextStyles: ErrorTextStyles; +} const CLASS_NAME = 'CollectContainer'; class ComposableContainer extends Container { @@ -45,21 +55,21 @@ class ComposableContainer extends Container { #elements: Record = {}; - #metaData: any; + #metaData: Metadata; - #elementGroup: any = { rows: [] }; + #elementGroup: ComposableElementGroup = { rows: [], styles: {}, errorTextStyles: {} }; - #elementsList:any = []; + #elementsList: Array = []; #context:Context; - #skyflowElements:any; + #skyflowElements: Array; #eventEmitter: EventEmitter; #isMounted: boolean = false; - #options: any; + #options: ContainerOptions; #containerElement:any; @@ -73,7 +83,12 @@ class ComposableContainer extends Container { #isSkyflowFrameReady: boolean = false; - constructor(options, metaData, skyflowElements, context) { + constructor( + metaData: Metadata, + skyflowElements: Array, + context: Context, + options: ContainerOptions, + ) { super(); this.#containerId = uuid(); this.#metaData = { @@ -114,7 +129,7 @@ class ComposableContainer extends Container { create = (input: CollectElementInput, options: CollectElementOptions = { required: false, - }) => { + }): ComposableElement => { validateCollectElementInput(input, this.#context.logLevel); const validations = formatValidations(input.validations); const formattedOptions = formatOptions(input.type, options, this.#context.logLevel); @@ -139,9 +154,9 @@ class ComposableContainer extends Container { }; #createMultipleElement = ( - multipleElements: any, + multipleElements: ComposableElementGroup, isSingleElementAPI: boolean = false, - ) => { + ): ComposableContainer => { const elements: any[] = []; this.#tempElements = deepClone(multipleElements); this.#tempElements.rows.forEach((row) => { @@ -307,7 +322,7 @@ class ComposableContainer extends Container { this.#containerElement.unmount(); }; - collect = (options: ICollectOptions = { tokens: true }) :Promise => { + collect = (options: ICollectOptions = { tokens: true }): Promise => { this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; if (this.#isSkyflowFrameReady) { return new Promise((resolve, reject) => { @@ -342,7 +357,7 @@ class ComposableContainer extends Container { this.#elementsList.forEach((element) => { elementIds.push({ frameId: this.#tempElements.elementName, - elementId: element.elementName, + elementId: element.elementName || '', }); }); bus @@ -412,7 +427,7 @@ class ComposableContainer extends Container { this.#elementsList.forEach((element) => { elementIds.push({ frameId: this.#tempElements.elementName, - elementId: element.elementName, + elementId: element.elementName || '', }); }); bus diff --git a/src/core/external/collect/compose-collect-element.ts b/src/core/external/collect/compose-collect-element.ts index 44a63a22..b3e7b7c3 100644 --- a/src/core/external/collect/compose-collect-element.ts +++ b/src/core/external/collect/compose-collect-element.ts @@ -19,7 +19,7 @@ class ComposableElement { #isUpdateCalled = false; - constructor(name, eventEmitter, iframeName) { + constructor(name: string, eventEmitter: EventEmitter, iframeName: string) { this.#elementName = name; this.#iframeName = iframeName; this.#eventEmitter = eventEmitter; diff --git a/src/core/external/common/iframe.ts b/src/core/external/common/iframe.ts index b59e8bc6..defcf630 100644 --- a/src/core/external/common/iframe.ts +++ b/src/core/external/common/iframe.ts @@ -9,17 +9,19 @@ import SkyflowError from '../../../libs/skyflow-error'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import { updateMetricObjectValue } from '../../../metrics/index'; import { METRIC_TYPES } from '../../constants'; +import { LogLevel } from '../../../index-node'; +import { Metadata } from '../../internal/internal-types'; export default class IFrame { name: string; - metadata: any; + metadata: Metadata; iframe: HTMLIFrameElement; container?: Element; - constructor(name, metadata, containerId, logLevel) { + constructor(name: string, metadata: Metadata, containerId: string, logLevel: LogLevel) { const clientDomain = metadata.clientDomain || ''; this.name = `${name}:${containerId}:${logLevel}:${btoa(clientDomain)}`; this.metadata = metadata; @@ -29,7 +31,7 @@ export default class IFrame { }); } - mount = (domElement, elementId?: string, data?: any) => { + mount = (domElement: HTMLElement | string, elementId?: string, data?: any) => { this.unmount(); try { if (typeof domElement === 'string') { diff --git a/src/core/external/common/skyflow-element.ts b/src/core/external/common/skyflow-element.ts index 7ab6161e..791077e1 100644 --- a/src/core/external/common/skyflow-element.ts +++ b/src/core/external/common/skyflow-element.ts @@ -2,19 +2,19 @@ Copyright (c) 2022 Skyflow, Inc. */ abstract class SkyflowElement { - abstract mount(domElementSelector); + abstract mount(domElementSelector: HTMLElement | string): void; - abstract unmount(); + abstract unmount(): void; - abstract setError(clientErrorText:string); + abstract setError(clientErrorText: string): void; - abstract resetError(); + abstract resetError(): void; - abstract setErrorOverride(customErrorText:string); + abstract setErrorOverride(customErrorText: string): void; - abstract iframeName(); + abstract iframeName(): string; - abstract getID(); + abstract getID(): string; } export default SkyflowElement; diff --git a/src/core/external/reveal/reveal-container.ts b/src/core/external/reveal/reveal-container.ts index 0961910f..fac8027a 100644 --- a/src/core/external/reveal/reveal-container.ts +++ b/src/core/external/reveal/reveal-container.ts @@ -8,6 +8,7 @@ import SkyflowError from '../../../libs/skyflow-error'; import uuid from '../../../libs/uuid'; import { ContainerType } from '../../../skyflow'; import { + ContainerOptions, Context, MessageType, RedactionType, RevealResponse, } from '../../../utils/common'; @@ -22,6 +23,7 @@ import { import Container from '../common/container'; import RevealElement from './reveal-element'; import properties from '../../../properties'; +import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; export interface IRevealElementInput { token?: string; @@ -50,7 +52,7 @@ class RevealContainer extends Container { #mountedRecords: { id: string }[] = []; - #metaData: any; + #metaData: Metadata; #containerId: string; @@ -62,15 +64,20 @@ class RevealContainer extends Container { #context: Context; - #skyflowElements: any; + #skyflowElements: Array; - #isMounted:any; + #isMounted: boolean = false; type:string = ContainerType.REVEAL; #isSkyflowFrameReady: boolean = false; - constructor(metaData, skyflowElements, context, options = {}) { + constructor( + metaData: Metadata, + skyflowElements: Array, + context: Context, + options?: ContainerOptions, + ) { super(); this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; this.#metaData = { diff --git a/src/core/external/reveal/reveal-element.ts b/src/core/external/reveal/reveal-element.ts index 259f1a90..a0264f8c 100644 --- a/src/core/external/reveal/reveal-element.ts +++ b/src/core/external/reveal/reveal-element.ts @@ -32,17 +32,19 @@ import { parameterizedString, printLog } from '../../../utils/logs-helper'; import { formatForRenderClient } from '../../../core-utils/reveal'; import properties from '../../../properties'; import { validateInitConfig, validateRenderElementRecord } from '../../../utils/validators'; +import { Metadata, RevealContainerProps } from '../../internal/internal-types'; +import EventEmitter from '../../../event-emitter'; const CLASS_NAME = 'RevealElement'; class RevealElement extends SkyflowElement { #iframe: IFrame; - #metaData: any; + #metaData: Metadata; #recordData: any; - #containerId: any; + #containerId: string; #isMounted:boolean = false; @@ -54,7 +56,7 @@ class RevealElement extends SkyflowElement { #readyToMount: boolean = false; - #eventEmitter:any; + #eventEmitter: EventEmitter; #isFrameReady: boolean; @@ -64,9 +66,14 @@ class RevealElement extends SkyflowElement { #isSkyflowFrameReady: boolean = false; - constructor(record: IRevealElementInput, + constructor( + record: IRevealElementInput, options: IRevealElementOptions = {}, - metaData: any, container: any, elementId: string, context: Context) { + metaData: Metadata, + container: RevealContainerProps, + elementId: string, + context: Context, + ) { super(); this.#elementId = elementId; this.#metaData = metaData; diff --git a/src/core/external/skyflow-container.ts b/src/core/external/skyflow-container.ts index e512de16..a5c9dfe2 100644 --- a/src/core/external/skyflow-container.ts +++ b/src/core/external/skyflow-container.ts @@ -57,7 +57,7 @@ class SkyflowContainer { #context: Context; - constructor(client, context) { + constructor(client: Client, context: Context) { this.#client = client; this.#containerId = this.#client.toJSON()?.metaData?.uuid || ''; this.#context = context; diff --git a/src/core/internal/internal-types/index.ts b/src/core/internal/internal-types/index.ts new file mode 100644 index 00000000..bb236b3f --- /dev/null +++ b/src/core/internal/internal-types/index.ts @@ -0,0 +1,83 @@ +import { ClientToJSON } from '../../../client'; +import EventEmitter from '../../../event-emitter'; +import { CollectContainer, ComposableContainer, RevealContainer } from '../../../index-node'; +import { ContainerType } from '../../../skyflow'; +import { CollectElementOptions, ICollectOptions } from '../../../utils/common'; +import { ElementType } from '../../constants'; +import SkyflowContainer from '../../external/skyflow-container'; + +export interface ElementInfo { + frameId: string; + elementId: string; +} + +export interface TokenizeDataInput extends ICollectOptions{ + type: string; + elementIds: Array; + containerId: string; +} + +export interface UploadFileDataInput extends ICollectOptions { + type: string; + elementIds: Array; + containerId: string; +} + +export interface BatchInsertRequestBody { + method: string; + quorum?: boolean; + tableName: string; + fields?: Record; + upsert?: string; + ID?: string; + tokenization?: boolean; + [key: string]: any; +} + +export interface ContainerProps { + containerId: string; + isMounted: boolean; + type: string; +} + +export interface RevealContainerProps { + containerId: string; + isMounted: boolean; + eventEmitter: EventEmitter; +} + +export interface InternalState { + isEmpty: boolean, + isValid: boolean, + isFocused: boolean, + isRequired: boolean, + name: string; + elementType: ElementType; + isComplete: boolean; + value: string | Blob | undefined; + selectedCardScheme: string; +} + +export interface FormattedCollectElementOptions extends CollectElementOptions { + [key: string]: any; +} + +export interface SkyflowElementProps { + id: string; + type: ElementType; + element: HTMLElement; + container: CollectContainer | RevealContainer | ComposableContainer; +} + +export interface ClientMetadata { + uuid: string, + clientDomain: string, + sdkVersion?: string; + sessionId?: string; +} + +export interface Metadata extends ClientMetadata { + clientJSON: ClientToJSON; + containerType: ContainerType; + skyflowContainer: SkyflowContainer; +} diff --git a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts index c06964de..95478cde 100644 --- a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts +++ b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts @@ -36,9 +36,20 @@ import { MessageType, Context, ISkyflowIdRecord, - IDeleteRecord, IGetOptions, - IInsertResponse, + IInsertRecordInput, + IInsertOptions, + UploadFilesResponse, + RevealResponse, + ErrorRecord, + InsertResponse, + CollectResponse, + IRevealResponseType, + GetResponse, + GetByIdResponse, + IDeleteResponseType, + IDeleteRecordInput, + IRenderResponseType, } from '../../../utils/common'; import { deleteData } from '../../../core-utils/delete'; import properties from '../../../properties'; @@ -48,6 +59,10 @@ import { } from '../../../utils/helpers'; import SkyflowError from '../../../libs/skyflow-error'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import { + BatchInsertRequestBody, ElementInfo, TokenizeDataInput, UploadFileDataInput, +} from '../internal-types'; +import IFrameFormElement from '../iframe-form'; const set = require('set-value'); @@ -121,7 +136,7 @@ class SkyflowFrameController { data.records as IRevealRecord[], this.#client, ).then( - (resolvedResult) => { + (resolvedResult: IRevealResponseType) => { printLog( parameterizedString( logs.infoLogs.FETCH_RECORDS_RESOLVED, @@ -132,7 +147,7 @@ class SkyflowFrameController { ); callback(resolvedResult); }, - (rejectedResult) => { + (rejectedResult: IRevealResponseType) => { printLog( parameterizedString(logs.errorLogs.FETCH_RECORDS_REJECTED), MessageType.ERROR, @@ -143,8 +158,8 @@ class SkyflowFrameController { }, ); } else if (data.type === PUREJS_TYPES.INSERT) { - this.insertData(data.records, data.options) - .then((result) => { + this.insertData(data.records as IInsertRecordInput, data.options as IInsertOptions) + .then((result: InsertResponse) => { printLog( parameterizedString( logs.infoLogs.INSERT_RECORDS_RESOLVED, @@ -153,10 +168,9 @@ class SkyflowFrameController { MessageType.LOG, this.#context.logLevel, ); - callback(result); }) - .catch((error) => { + .catch((error: InsertResponse) => { printLog( parameterizedString(logs.errorLogs.INSERT_RECORDS_REJECTED), MessageType.ERROR, @@ -168,7 +182,7 @@ class SkyflowFrameController { fetchRecordsGET( data.records as IGetRecord[], this.#client, data.options as IGetOptions, ).then( - (resolvedResult) => { + (resolvedResult: GetResponse) => { printLog( parameterizedString(logs.infoLogs.GET_RESOLVED, CLASS_NAME), MessageType.LOG, @@ -177,7 +191,7 @@ class SkyflowFrameController { callback(resolvedResult); }, - (rejectedResult) => { + (rejectedResult: GetResponse) => { printLog(parameterizedString( logs.errorLogs.GET_REJECTED, ), @@ -192,7 +206,7 @@ class SkyflowFrameController { data.records as ISkyflowIdRecord[], this.#client, ).then( - (resolvedResult) => { + (resolvedResult: GetByIdResponse) => { printLog( parameterizedString( logs.infoLogs.GET_BY_SKYFLOWID_RESOLVED, @@ -204,7 +218,7 @@ class SkyflowFrameController { callback(resolvedResult); }, - (rejectedResult) => { + (rejectedResult: GetByIdResponse) => { printLog( parameterizedString(logs.errorLogs.GET_BY_SKYFLOWID_REJECTED), MessageType.ERROR, @@ -216,11 +230,11 @@ class SkyflowFrameController { ); } else if (data.type === PUREJS_TYPES.DELETE) { deleteData( - data.records as IDeleteRecord[], - data.options, + data.records as IDeleteRecordInput, + data.options || {}, this.#client, ).then( - (resolvedResult) => { + (resolvedResult: IDeleteResponseType) => { printLog( parameterizedString( logs.infoLogs.DELETE_RESOLVED, @@ -232,7 +246,7 @@ class SkyflowFrameController { callback(resolvedResult); }, - ).catch((rejectedResult) => { + ).catch((rejectedResult: IDeleteResponseType) => { printLog( parameterizedString( logs.errorLogs.DELETE_RECORDS_REJECTED, @@ -278,22 +292,34 @@ class SkyflowFrameController { MessageType.LOG, this.#context.logLevel, ); - this.tokenize(data) - .then((response) => { + const tokenizeDataInput: TokenizeDataInput = { + ...data, + type: data.type, + elementIds: data.elementIds as Array, + containerId: data.containerId as string, + }; + this.tokenize(tokenizeDataInput) + .then((response: CollectResponse) => { callback(response); }) - .catch((error) => { + .catch((error: CollectResponse) => { callback({ error }); }); } else if (data.type === COLLECT_TYPES.FILE_UPLOAD) { printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.FILE_UPLOAD), MessageType.LOG, this.#context.logLevel); - this.parallelUploadFiles(data) - .then((response) => { + const uploadFilesDataInput = { + ...data, + type: data.type, + elementIds: data.elementIds as string[], + containerId: data.containerId as string, + }; + this.parallelUploadFiles(uploadFilesDataInput) + .then((response: UploadFilesResponse) => { callback(response); }) - .catch((error) => { + .catch((error: UploadFilesResponse) => { callback({ error }); }); } @@ -331,7 +357,7 @@ class SkyflowFrameController { printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.REVEAL_REQUEST), MessageType.LOG, this.#context.logLevel); - this.revealData(data.records as any, data.containerId).then( + this.revealData(data.records as IRevealRecord[], data.containerId as string).then( (resolvedResult) => { callback(resolvedResult); }, @@ -343,7 +369,7 @@ class SkyflowFrameController { printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), MessageType.LOG, this.#context.logLevel); - this.renderFile(data.records, data.iframeName).then( + this.renderFile(data.records as IRevealRecord, data.iframeName as string).then( (resolvedResult) => { callback( resolvedResult, @@ -357,7 +383,7 @@ class SkyflowFrameController { }); } - static init(clientId) { + static init(clientId: string): SkyflowFrameController { const trackingStatus = getValueFromName(window.name, 3) === 'true'; if (trackingStatus) { const scriptTag = document.createElement('script'); @@ -367,7 +393,7 @@ class SkyflowFrameController { return new SkyflowFrameController(clientId); } - revealData(revealRecords: IRevealRecord[], containerId) { + revealData(revealRecords: IRevealRecord[], containerId: string): Promise { const id = containerId; return new Promise((resolve, reject) => { fetchRecordsByTokenId(revealRecords, this.#client).then( @@ -397,13 +423,15 @@ class SkyflowFrameController { }); } - insertData(records, options) { - const requestBody = constructInsertRecordRequest(records, options); + insertData(records: IInsertRecordInput, options: IInsertOptions): Promise { + const requestBody: Array = constructInsertRecordRequest( + records, options, + ); return new Promise((rootResolve, rootReject) => { getAccessToken(this.#clientId).then((authToken) => { this.#client .request({ - body: { records: requestBody }, + body: JSON.stringify({ records: requestBody }), requestMethod: 'POST', url: `${this.#client.config.vaultURL}/v1/vaults/${ @@ -417,7 +445,7 @@ class SkyflowFrameController { rootResolve( constructInsertRecordResponse( response, - options?.tokens, + options?.tokens ?? true, records?.records, ), ); @@ -431,7 +459,7 @@ class SkyflowFrameController { }); } - renderFile(data, iframeName) { + renderFile(data: IRevealRecord, iframeName: string): Promise { return new Promise((resolve, reject) => { try { getFileURLFromVaultBySkyflowID(data, this.#client) @@ -472,8 +500,8 @@ class SkyflowFrameController { }); } - tokenize = (options) => { - const id = options.containerId; + tokenize = (options: TokenizeDataInput): Promise => { + const id: string = options.containerId; if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); const insertResponseObject: any = {}; const updateResponseObject: any = {}; @@ -584,11 +612,11 @@ class SkyflowFrameController { } } } - let finalInsertRequest; + let finalInsertRequest: Array; let finalInsertRecords; let finalUpdateRecords; - let insertResponse: IInsertResponse; - let updateResponse: IInsertResponse; + let insertResponse: InsertResponse; + let updateResponse: InsertResponse; let insertErrorResponse: any; let updateErrorResponse; let insertDone = false; @@ -604,15 +632,13 @@ class SkyflowFrameController { }); } const client = this.#client; - const sendRequest = () => new Promise((rootResolve, rootReject) => { + const sendRequest = (): Promise => new Promise((rootResolve, rootReject) => { const clientId = client.toJSON()?.metaData?.uuid || ''; getAccessToken(clientId).then((authToken) => { if (finalInsertRequest.length !== 0) { client .request({ - body: { - records: finalInsertRequest, - }, + body: JSON.stringify({ records: finalInsertRequest }), requestMethod: 'POST', url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, headers: { @@ -623,7 +649,7 @@ class SkyflowFrameController { .then((response: any) => { insertResponse = constructInsertRecordResponse( response, - options.tokens, + options.tokens ?? true, finalInsertRecords.records, ); insertDone = true; @@ -634,12 +660,14 @@ class SkyflowFrameController { if (updateErrorResponse.records === undefined) { updateErrorResponse.records = insertResponse.records; } else { - updateErrorResponse.records = insertResponse.records + updateErrorResponse.records = (insertResponse.records || []) .concat(updateErrorResponse.records); } rootReject(updateErrorResponse); } else if (updateDone && updateResponse !== undefined) { - rootResolve({ records: insertResponse.records.concat(updateResponse.records) }); + rootResolve( + { records: (insertResponse.records || []).concat(updateResponse.records || []) }, + ); } }) .catch((error) => { @@ -680,7 +708,9 @@ class SkyflowFrameController { rootResolve(updateResponse); } if (insertDone && insertResponse !== undefined) { - rootResolve({ records: insertResponse.records.concat(updateResponse.records) }); + rootResolve( + { records: (insertResponse.records || []).concat(updateResponse.records || []) }, + ); } else if (insertDone && insertErrorResponse !== undefined) { const errors = insertErrorResponse.errors; const records = updateResponse.records; @@ -696,7 +726,7 @@ class SkyflowFrameController { if (updateErrorResponse.records === undefined) { updateErrorResponse.records = insertResponse.records; } else { - updateErrorResponse.records = insertResponse.records + updateErrorResponse.records = (insertResponse.records || []) .concat(updateErrorResponse.records); } rootReject(updateErrorResponse); @@ -719,7 +749,8 @@ class SkyflowFrameController { }); }; - parallelUploadFiles = (options) => new Promise((rootResolve, rootReject) => { + parallelUploadFiles = (options: UploadFileDataInput): + Promise => new Promise((rootResolve, rootReject) => { const id = options.containerId; const promises: Promise[] = []; for (let i = 0; i < options.elementIds.length; i += 1) { @@ -740,16 +771,13 @@ class SkyflowFrameController { Promise.allSettled( promises, ).then((resultSet) => { - const fileUploadResponse: Record[] = []; - const errorResponse: Record[] = []; + const fileUploadResponse: { skyflow_id: string }[] = []; + const errorResponse: { error: ErrorRecord }[] = []; resultSet.forEach((result) => { if (result.status === 'fulfilled') { if (result.value !== undefined && result.value !== null) { - if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { - errorResponse.push(result.value); - } else { - fileUploadResponse.push(result.value); - } + const parsedResultValue = JSON.parse(result.value as string); + fileUploadResponse.push({ skyflow_id: parsedResultValue.skyflow_id }); } } else if (result.status === 'rejected') { errorResponse.push(result.reason); @@ -762,7 +790,7 @@ class SkyflowFrameController { }); }); - uploadFiles = (fileElement) => { + uploadFiles = (fileElement: IFrameFormElement) => { if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); const fileUploadObject: any = {}; @@ -806,7 +834,8 @@ class SkyflowFrameController { } const client = this.#client; - const sendRequest = () => new Promise((rootResolve, rootReject) => { + const sendRequest = (): + Promise => new Promise((rootResolve, rootReject) => { const clientId = client.toJSON()?.metaData?.uuid || ''; getAccessToken(clientId).then((authToken) => { client diff --git a/src/libs/element-options.ts b/src/libs/element-options.ts index 3ee11b57..360c6258 100644 --- a/src/libs/element-options.ts +++ b/src/libs/element-options.ts @@ -11,13 +11,16 @@ import { DEFAULT_EXPIRATION_YEAR_FORMAT, DEFAULT_INPUT_FORMAT_TRANSLATION, ELEMENTS, + ElementType, INPUT_FORMATTING_NOT_SUPPORTED_ELEMENT_TYPES, INPUT_STYLES, } from '../core/constants'; import CollectElement from '../core/external/collect/collect-element'; import ComposableElement from '../core/external/collect/compose-collect-element'; +import { FormattedCollectElementOptions } from '../core/internal/internal-types'; import { - IValidationRule, MessageType, ValidationRuleType, + CollectElementOptions, + IValidationRule, LogLevel, MessageType, ValidationRuleType, } from '../utils/common'; import SKYFLOW_ERROR_CODE from '../utils/constants'; import logs from '../utils/logs'; @@ -277,8 +280,12 @@ IValidationRule[] | undefined => { }); }; -export const formatOptions = (elementType, options, logLevel) => { - let formattedOptions = { +export const formatOptions = ( + elementType: ElementType, + options: CollectElementOptions, + logLevel: LogLevel, +) => { + let formattedOptions: FormattedCollectElementOptions = { required: false, ...options, }; @@ -349,7 +356,7 @@ export const formatOptions = (elementType, options, logLevel) => { } formattedOptions = { ...formattedOptions, - format: isvalidFormat ? formattedOptions.format.toUpperCase() + format: (isvalidFormat && formattedOptions.format) ? formattedOptions.format.toUpperCase() : DEFAULT_EXPIRATION_DATE_FORMAT, }; delete formattedOptions?.translation; @@ -366,7 +373,7 @@ export const formatOptions = (elementType, options, logLevel) => { } formattedOptions = { ...formattedOptions, - format: isvalidFormat ? formattedOptions.format.toUpperCase() + format: (isvalidFormat && formattedOptions.format) ? formattedOptions.format.toUpperCase() : DEFAULT_EXPIRATION_YEAR_FORMAT, }; delete formattedOptions?.translation; diff --git a/src/libs/jss-styles.ts b/src/libs/jss-styles.ts index 81d0749c..daced7d7 100644 --- a/src/libs/jss-styles.ts +++ b/src/libs/jss-styles.ts @@ -6,7 +6,7 @@ import preset from 'jss-preset-default'; jss.setup(preset()); -export default function getCssClassesFromJss(styles, name) { +export default function getCssClassesFromJss(styles, name: string) { const createGenerateId = () => (rule) => `SkyflowElement-${name}-${rule.key}`; jss.setup({ createGenerateId }); const cssStyle = jss.createStyleSheet(styles); diff --git a/src/libs/styles.ts b/src/libs/styles.ts index 0f2fa8e0..886100d0 100644 --- a/src/libs/styles.ts +++ b/src/libs/styles.ts @@ -58,11 +58,9 @@ export const getFlexGridStyles = (obj: any) => { const styles = { 'align-items': obj['align-items'] || 'stretch', 'justify-content': obj['justify-content'] || 'flex-start', - height: - 'auto' - || `calc(100% + ${ - Number.parseInt(spacingValueAndUnit[0], 10) * 2 + spacingValueAndUnit[1] - })`, + height: obj.height === 'auto' ? 'auto' : `calc(100% + ${ + Number.parseInt(spacingValueAndUnit[0], 10) * 2 + spacingValueAndUnit[1] + })`, width: `calc(100% + ${ Number.parseInt(spacingValueAndUnit[0], 10) * 2 + spacingValueAndUnit[1] }))`, diff --git a/src/skyflow.ts b/src/skyflow.ts index 50333549..447dff8c 100644 --- a/src/skyflow.ts +++ b/src/skyflow.ts @@ -47,25 +47,34 @@ import { formatVaultURL, checkAndSetForCustomUrl } from './utils/helpers'; import ComposableContainer from './core/external/collect/compose-collect-container'; import { validateComposableContainerOptions } from './utils/validators'; import ThreeDS from './core/external/threeds/threeds'; +import { ClientMetadata, SkyflowElementProps } from './core/internal/internal-types'; export enum ContainerType { COLLECT = 'COLLECT', REVEAL = 'REVEAL', COMPOSABLE = 'COMPOSABLE', } +export interface SkyflowConfigOptions { + logLevel?: LogLevel; + env?: Env; + trackingKey?: string; + trackMetrics?: boolean; + customElementsURL?: string; +} export interface ISkyflow { vaultID?: string; vaultURL?: string; getBearerToken: () => Promise; - options?: Record; + options?: SkyflowConfigOptions; } + const CLASS_NAME = 'Skyflow'; class Skyflow { #client: Client; #uuid: string = uuid(); - #metadata = { + #metadata: ClientMetadata = { uuid: this.#uuid, clientDomain: window.location.origin, }; @@ -80,7 +89,7 @@ class Skyflow { #env:Env; - #skyflowElements: any; + #skyflowElements: Array; constructor(config: ISkyflow) { const localSDKversion = localStorage.getItem('sdk_version') || ''; @@ -94,11 +103,11 @@ class Skyflow { ); this.#logLevel = config?.options?.logLevel || LogLevel.ERROR; this.#env = config?.options?.env || Env.PROD; - this.#skyflowElements = {}; + this.#skyflowElements = []; this.#skyflowContainer = new SkyflowContainer(this.#client, { logLevel: this.#logLevel, env: this.#env }); - const cb = (data, callback) => { + const cb = (data, callback: Function) => { printLog(parameterizedString(logs.infoLogs.CAPTURED_BEARER_TOKEN_EVENT, CLASS_NAME), MessageType.LOG, this.#logLevel); @@ -170,14 +179,14 @@ class Skyflow { container(type: ContainerType, options?: ContainerOptions) { switch (type) { case ContainerType.COLLECT: { - const collectContainer = new CollectContainer(options, { + const collectContainer = new CollectContainer({ ...this.#metadata, clientJSON: this.#client.toJSON(), containerType: type, skyflowContainer: this.#skyflowContainer, }, this.#skyflowElements, - { logLevel: this.#logLevel, env: this.#env }); + { logLevel: this.#logLevel, env: this.#env }, options); printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), MessageType.LOG, this.#logLevel); @@ -191,26 +200,29 @@ class Skyflow { skyflowContainer: this.#skyflowContainer, }, this.#skyflowElements, - { logLevel: this.#logLevel }, options); + { logLevel: this.#logLevel, env: this.#env }, options); printLog(parameterizedString(logs.infoLogs.REVEAL_CONTAINER_CREATED, CLASS_NAME), MessageType.LOG, this.#logLevel); return revealContainer; } case ContainerType.COMPOSABLE: { - validateComposableContainerOptions(options); - const collectContainer = new ComposableContainer(options, { - ...this.#metadata, - clientJSON: this.#client.toJSON(), - containerType: type, - skyflowContainer: this.#skyflowContainer, - }, - this.#skyflowElements, - { logLevel: this.#logLevel, env: this.#env }); + validateComposableContainerOptions(options!); + const composableContainer = new ComposableContainer( + { + ...this.#metadata, + clientJSON: this.#client.toJSON(), + containerType: type, + skyflowContainer: this.#skyflowContainer, + }, + this.#skyflowElements, + { logLevel: this.#logLevel, env: this.#env }, + options!, + ); printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), MessageType.LOG, this.#logLevel); - return collectContainer; + return composableContainer; } default: diff --git a/src/utils/bus-events/index.ts b/src/utils/bus-events/index.ts index 80c3d329..4cfe1952 100644 --- a/src/utils/bus-events/index.ts +++ b/src/utils/bus-events/index.ts @@ -5,7 +5,7 @@ import bus from 'framebus'; import { ELEMENT_EVENTS_TO_IFRAME, FRAME_ELEMENT } from '../../core/constants'; import properties from '../../properties'; -export function getAccessToken(clientId) { +export function getAccessToken(clientId: string) { return new Promise((resolve, reject) => { bus.emit(ELEMENT_EVENTS_TO_IFRAME.GET_BEARER_TOKEN + clientId, {}, (data:any) => { diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index 5932f888..e98fbf10 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -1,9 +1,9 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ import { IUpsertOptions } from '../../core-utils/collect'; import { CardType, ElementType } from '../../core/constants'; -/* -Copyright (c) 2022 Skyflow, Inc. -*/ declare global { interface Window { CoralogixRum: any; @@ -198,6 +198,7 @@ export interface DetokenizeResponse extends IRevealResponseType {} export interface InsertResponseRecords { fields: Record, table: string, + skyflow_id?: string, } export interface ErrorRecord { @@ -310,19 +311,19 @@ export interface ICollectOptions { } export interface UploadFilesResponse { - fileUploadResponse: [{ skyflow_id: string }], - errorResponse: [{ error: ErrorRecord }], + fileUploadResponse?: { skyflow_id: string }[], + errorResponse?: { error: ErrorRecord }[], } export interface RevealResponse { - success?: { + success?: Array<{ token: string, valueType: string, - }, - errors?: { + }>, + errors?: Array<{ error: ErrorRecord, token: string, - } + }> } export interface RenderFileResponse { diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 9802b370..1bb3eee0 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -33,7 +33,7 @@ export function removeSpaces(inputString:string) { return inputString.trim().replace(/[\s-]/g, ''); } -export function formatVaultURL(vaultURL) { +export function formatVaultURL(vaultURL?: string) { if (typeof vaultURL !== 'string') return vaultURL; return (vaultURL?.trim().slice(-1) === '/') ? vaultURL.slice(0, -1) : vaultURL.trim(); } @@ -42,7 +42,7 @@ export function checkIfDuplicateExists(arr) { return new Set(arr).size !== arr.length; } -export const appendZeroToOne = (value) => { +export const appendZeroToOne = (value: string) => { if (value.length === 1 && Number(value) === 1) { return { isAppended: true, @@ -52,14 +52,14 @@ export const appendZeroToOne = (value) => { return { isAppended: false, value }; }; -export const appendMonthFourDigitYears = (value) => { +export const appendMonthFourDigitYears = (value: string) => { if (value.length === 6 && Number(value.charAt(5)) === 1) { return { isAppended: true, value: `${value.substring(0, 5)}0${value.charAt(5)}` }; } return { isAppended: false, value }; }; -export const appendMonthTwoDigitYears = (value) => { +export const appendMonthTwoDigitYears = (value: string) => { const lastChar = (value.length > 0 && value.charAt(value.length - 1)) || ''; if (value.length === 4 && Number(lastChar) === 1) { return { isAppended: true, value: `${value.substring(0, 3)}0${lastChar}` }; @@ -223,7 +223,7 @@ export const fileValidation = (value, required: Boolean = false, fileElement) => return true; }; -export const vaildateFileName = (name) => ALLOWED_NAME_FOR_FILE.test(name); +export const vaildateFileName = (name: string) => ALLOWED_NAME_FOR_FILE.test(name); export const styleToString = (style) => Object.keys(style).reduce((acc, key) => ( `${acc + key.split(/(?=[A-Z])/).join('-').toLowerCase()}:${style[key]};` diff --git a/src/utils/validators/index.ts b/src/utils/validators/index.ts index c3d26997..5630f9d7 100644 --- a/src/utils/validators/index.ts +++ b/src/utils/validators/index.ts @@ -22,6 +22,8 @@ import { IDeleteRecordInput, IGetOptions, CollectElementInput, + LogLevel, + ContainerOptions, } from '../common'; import SKYFLOW_ERROR_CODE from '../constants'; import { appendZeroToOne } from '../helpers'; @@ -59,7 +61,7 @@ export const detectCardType = (cardNumber: string = '') => { return detectedType; }; -const getYearAndMonthBasedOnFormat = (cardDate, format: string) => { +const getYearAndMonthBasedOnFormat = (cardDate: string, format: string) => { const [part1, part2] = cardDate.split('/'); switch (format) { case 'MM/YY': return { month: appendZeroToOne(part1).value, year: 2000 + Number(part2) }; @@ -74,8 +76,8 @@ export const validateExpiryDate = (date: string, format: string) => { if (date.trim().length === 0) return true; if (!date.includes('/')) return false; const { month, year } = getYearAndMonthBasedOnFormat(date, format); - if (format.endsWith('YYYY') && year.length !== 4) { return false; } - const expiryDate = new Date(year, month, 0); + if (format.endsWith('YYYY') && year.toString().length !== 4) { return false; } + const expiryDate = new Date(Number(year), Number(month), 0); expiryDate.setHours(23, 59, 59, 999); const today = new Date(); @@ -547,7 +549,7 @@ export const validateInitConfig = (initConfig: ISkyflow) => { } }; -export const validateCollectElementInput = (input: CollectElementInput, logLevel) => { +export const validateCollectElementInput = (input: CollectElementInput, logLevel: LogLevel) => { if (!Object.prototype.hasOwnProperty.call(input, 'type')) { throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_ELEMENT_TYPE, [], true); } @@ -637,7 +639,7 @@ export const validateUpsertOptions = (upsertOptions) => { }); }; -export const validateComposableContainerOptions = (options) => { +export const validateComposableContainerOptions = (options: ContainerOptions) => { if (!options) { throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_COMPOSABLE_CONTAINER_OPTIONS, [], true); } diff --git a/tests/client.test.ts b/tests/client.test.ts new file mode 100644 index 00000000..59b58cfe --- /dev/null +++ b/tests/client.test.ts @@ -0,0 +1,411 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import assert from "assert"; +import Client, { IClientRequest } from "../src/client"; +import { ClientMetadata } from "../src/core/internal/internal-types"; +import { ISkyflow } from "../src/skyflow"; + +const skyflowConfig: ISkyflow = { + vaultID: "e20afc3ae1b54f0199f24130e51e0c11", + vaultURL: "https://testurl.com", + getBearerToken: jest.fn(), + options: { trackMetrics: true, trackingKey: "key" }, +}; + +const metaData: ClientMetadata = { + uuid: "1234", + clientDomain: "http://abc.com", +}; + +describe("Client Class", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + test("Client fromJson method", () => { + const testClientObject = Client.fromJSON({ + config: skyflowConfig, + metaData: metaData, + }); + expect(testClientObject).toBeInstanceOf(Client); + }); + + test("client toJSON", () => { + const testClient = new Client(skyflowConfig, metaData); + const testClient2 = testClient.toJSON(); + expect(testClient2.metaData).toBeDefined(); + }); + + test("Client Request Method without errors", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 200, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest + .fn() + .mockImplementation(() => "content-type: application/json"), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient.request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + "content-type": "application/json", + Auth: "eyde.ed.ewe", + }, + body: JSON.stringify({ + key: "value", + }), + }); + expect(xhrMock.open).toBeCalledWith("GET", "https://example-test.com"); + expect(xhrMock.setRequestHeader).toBeCalledWith("Auth", "eyde.ed.ewe"); + expect(xhrMock.send).toBeCalledWith( + JSON.stringify({ + key: "value", + }) + ); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with url-formencoded content-type", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 200, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest.fn().mockImplementation( + () => `content-type: application/json + x-request-id: req_123` + ), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient.request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + body: JSON.stringify({ + key: "value", + }), + }); + expect(xhrMock.setRequestHeader).toBeCalledWith( + "content-type", + "application/x-www-form-urlencoded" + ); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with form-data content-type", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 200, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest.fn().mockImplementation( + () => `content-type: application/json + x-request-id: req_123` + ), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient.request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + Auth: "eyde.ed.ewe", + "content-type": "multipart/form-data", + }, + body: JSON.stringify({ + key: "value", + }), + }); + expect(xhrMock.setRequestHeader).toBeCalledWith("Auth", "eyde.ed.ewe"); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with url-formencoded content-type and navigator as null", () => { + try { + const mockNavigator = { + userAgent: "", // Set userAgent to null or any desired value + // Add other properties or methods as needed for your test case + }; + + // Replace the original navigator object with the mock navigator + Object.defineProperty(window, "navigator", { + value: mockNavigator, + configurable: true, + enumerable: true, + writable: false, + }); + + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 200, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest.fn().mockImplementation( + () => `content-type: application/json + x-request-id: req_123` + ), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient.request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + body: JSON.stringify({ + key: "value", + }), + }); + expect(xhrMock.setRequestHeader).toBeCalledWith( + "content-type", + "application/x-www-form-urlencoded" + ); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with error 1", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 401, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest.fn().mockImplementation( + () => `content-type: text/plain + x-request-id: req_123` + ), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient + .request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + Auth: "eyde.ed.ewe", + }, + body: JSON.stringify({ + key: "value", + }), + }) + .catch((err) => { + expect(err).toBeDefined(); + }); + expect(xhrMock.open).toBeCalledWith("GET", "https://example-test.com"); + expect(xhrMock.setRequestHeader).toBeCalledWith("Auth", "eyde.ed.ewe"); + expect(xhrMock.send).toBeCalledWith( + JSON.stringify({ + key: "value", + }) + ); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with error 2", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 401, + response: JSON.stringify({ + error: { message: "Something went wrong" }, + }), + getAllResponseHeaders: jest + .fn() + .mockImplementation(() => "content-type: application/json"), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient + .request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + Auth: "eyde.ed.ewe", + }, + body: JSON.stringify({ + key: "value", + }), + }) + .catch((err) => { + expect(err).toBeDefined(); + }); + expect(xhrMock.open).toBeCalledWith("GET", "https://example-test.com"); + expect(xhrMock.setRequestHeader).toBeCalledWith("Auth", "eyde.ed.ewe"); + expect(xhrMock.send).toBeCalledWith( + JSON.stringify({ + key: "value", + }) + ); + } catch (err) { + console.log(err); + } + }); +}); diff --git a/tests/core-utils/collect.test.ts b/tests/core-utils/collect.test.ts new file mode 100644 index 00000000..ddb4bf0c --- /dev/null +++ b/tests/core-utils/collect.test.ts @@ -0,0 +1,168 @@ +import { + getUpsertColumn, + constructElementsInsertReq, + checkForValueMatch, +} from "../../src/core-utils/collect"; +import IFrameFormElement from "../../src/core/internal/iframe-form"; +import { + ICollectOptions, + IUpsertOption, + IValidationRule, + ValidationRuleType, +} from "../../src/utils/common"; +import SKYFLOW_ERROR_CODE from "../../src/utils/constants"; +import { parameterizedString } from "../../src/utils/logs-helper"; + +describe("Testing getUpsertColumn method", () => { + const options: ICollectOptions = { + upsert: [ + { + table: "test", + column: "column", + } as IUpsertOption, + ], + }; + + test("return unique column", () => { + const fnResponse = getUpsertColumn("test", options.upsert); + expect(fnResponse).toStrictEqual("column"); + }); + + test("return empty column", () => { + const fnResponse = getUpsertColumn("testTwo", options.upsert); + expect(fnResponse).toStrictEqual(""); + }); + + test("upsert options as undefined", () => { + const fnResponse = getUpsertColumn("test", undefined); + expect(fnResponse).toStrictEqual(""); + }); +}); + +let req = { + table1: { + fields: { + cvv: "122", + }, + }, +}; + +let update = { + table1: { + fields: { + cvv: "122", + }, + }, +}; + +let update2 = { + table1: { + fields: { + column: "122", + }, + }, +}; + +const options: ICollectOptions = { + tokens: true, + additionalFields: { + records: [ + { + table: "table1", + fields: { + name: "name", + }, + }, + ], + }, +}; + +const options2: ICollectOptions = { + tokens: true, + additionalFields: { + records: [ + { + table: "table1", + fields: { + column: "122", + skyflowID: "table1", + }, + }, + ], + }, +}; + +describe("Testing constructElementsInsertReq method", () => { + test("constructElementsInsertReq error 1", () => { + try { + constructElementsInsertReq(req, update, options); + } catch (err) { + expect(err.error.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT_ADDITIONAL_FIELDS.description + ) + ); + } + }); + + test("constructElementsInsertReq error 2", () => { + try { + constructElementsInsertReq(req, update2, options); + } catch (err) { + expect(err.error.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT.description, + "name", + "table1" + ) + ); + } + }); + + test("constructElementsInsertReq error 2", () => { + try { + constructElementsInsertReq(req, update2, options2); + } catch (err) { + expect(err.error.description).toEqual( + parameterizedString(SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT.description) + ); + } + }); +}); + +class MockIFrameFormElement { + state = { value: "testValue" }; + + isMatchEqual(index: number, value: string, rule: IValidationRule) { + return index % 2 === 0; + } +} + +describe("Testing checkForValueMatch method", () => { + let element: MockIFrameFormElement; + + beforeEach(() => { + element = new MockIFrameFormElement(); + }); + + it("should return true when an ELEMENT_VALUE_MATCH_RULE type is found and isMatchEqual returns false", () => { + const validations = [ + { type: ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, params: {} }, + ]; + jest.spyOn(element, "isMatchEqual").mockReturnValue(false); + expect(checkForValueMatch(validations, element as IFrameFormElement)).toBe( + true + ); + }); + + it("should return false when an ELEMENT_VALUE_MATCH_RULE type is found but isMatchEqual returns true", () => { + const validations = [ + { type: ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, params: {} }, + ]; + jest.spyOn(element, "isMatchEqual").mockReturnValue(true); + + expect(checkForValueMatch(validations, element as IFrameFormElement)).toBe( + false + ); + }); +}); diff --git a/tests/core/external/collect/collect-container.test.js b/tests/core/external/collect/collect-container.test.js index 26b58ae2..cbb0d666 100644 --- a/tests/core/external/collect/collect-container.test.js +++ b/tests/core/external/collect/collect-container.test.js @@ -222,7 +222,7 @@ describe('Collect container', () => { document.body.innerHTML = ''; }); it('should throw error when collect call made with no elements ', () => { - const collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.collect().then().catch(err => { expect(err).toBeDefined(); @@ -230,7 +230,7 @@ describe('Collect container', () => { }) }); it('should throw error when collect call made with no elements case2 ', () => { - const collectContainer = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const collectContainer = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.collect().then().catch(err => { expect(err).toBeDefined(); @@ -238,7 +238,7 @@ describe('Collect container', () => { }) }); it('should throw error when uploadfiles call made with no elements ', () => { - const collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.uploadFiles().then().catch(err => { expect(err).toBeDefined(); @@ -246,7 +246,7 @@ describe('Collect container', () => { }) }); it('should throw error when uploadfiles call made with no elements ', () => { - const collectContainer = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const collectContainer = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.uploadFiles().then().catch(err => { expect(err).toBeDefined(); @@ -255,7 +255,7 @@ describe('Collect container', () => { }); it("container collect success", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -299,7 +299,7 @@ describe('Collect container', () => { }); it("container collect case when tokens are invalid", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -326,7 +326,7 @@ describe('Collect container', () => { }); it("container collect case when additional fields are invalid", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -353,7 +353,7 @@ describe('Collect container', () => { }); it("container collect case when upsert are invalid", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -379,7 +379,7 @@ describe('Collect container', () => { }) }); it("container collect case when elements are invalid", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -444,7 +444,7 @@ describe('Collect container', () => { }); it('should resolve successfully when collect is called and isSkyflowFrameReady is false', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -512,7 +512,7 @@ describe('Collect container', () => { }); }); it('should throw error when collect is called and isSkyflowFrameReady is false', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -581,7 +581,7 @@ describe('Collect container', () => { }); }); it('should throw error when collect is called and isSkyflowFrameReady is false and tokens is invalid', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -630,7 +630,7 @@ describe('Collect container', () => { }); it('should throw error when collect is called and isSkyflowFrameReady is false and upsert is invalid', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -679,7 +679,7 @@ describe('Collect container', () => { }); it('should throw error when collect is called and isSkyflowFrameReady is false and additionalFields is invalid', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -728,7 +728,7 @@ describe('Collect container', () => { }); it('should throw error when collect is called and isSkyflowFrameReady is false and additionalFields is invalid', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -780,7 +780,7 @@ describe('Collect container', () => { }); it('element type radio or checkox created', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -829,7 +829,7 @@ describe('Collect container', () => { }); it('should successfully upload files when elements are mounted', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); @@ -872,7 +872,7 @@ describe('Collect container', () => { }); }); it('should throw error when elements are not created', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const uploadPromise = container.uploadFiles(); @@ -883,7 +883,7 @@ describe('Collect container', () => { }); }); it('should throw error when elements are not created and skyflow frame controller not ready', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const uploadPromise = container.uploadFiles(); @@ -894,7 +894,7 @@ describe('Collect container', () => { }); }); it('should throw error when elements are created but not mounted', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -911,7 +911,7 @@ describe('Collect container', () => { }); it('should successfully upload files when elements are mounted', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); @@ -935,14 +935,14 @@ describe('Collect container', () => { }); it('should throw an error if elements are not mounted', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); await expect(container.uploadFiles()).rejects.toThrow(SkyflowError); }); it('should throw an error if elements are not mounted and skyflow frame not ready', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -954,7 +954,7 @@ describe('Collect container', () => { expect(response).rejects.toThrow(SkyflowError); }); it('should throw an error if elements are not mounted when skyflow frame controller is not ready', () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); Object.defineProperty(container, '#isSkyflowFrameReady', { @@ -974,7 +974,7 @@ describe('Collect container', () => { }); it('should handle errors during file upload', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); @@ -997,7 +997,7 @@ describe('Collect container', () => { }); it('should not emit events when isSkyflowFrameReady is false', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -1019,7 +1019,7 @@ describe('Collect container', () => { }); it('should resolve successfully when file upload is successful', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -1071,7 +1071,7 @@ describe('Collect container', () => { }); it('Invalid element type', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, type: 'abc' }); } catch (err) { @@ -1080,7 +1080,7 @@ describe('Collect container', () => { }); it('Invalid table', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1092,7 +1092,7 @@ describe('Collect container', () => { }); it('Invalid column', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1104,7 +1104,7 @@ describe('Collect container', () => { }); it('Invalid validation params, missing element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1120,7 +1120,7 @@ describe('Collect container', () => { }); it('Invalid validation params, invalid collect element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1137,7 +1137,7 @@ describe('Collect container', () => { } }); it('Invalid validation params, invalid collect element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1154,7 +1154,7 @@ describe('Collect container', () => { } }); it('valid validation params, regex match rule', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1174,7 +1174,7 @@ describe('Collect container', () => { it('create valid Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let cvv; try { cvv = container.create(cvvElement); @@ -1186,7 +1186,7 @@ describe('Collect container', () => { }); it('test default options for card_number', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let card_number; try { card_number = container.create(cardNumberElement); @@ -1199,7 +1199,7 @@ describe('Collect container', () => { it('test invalid option for EXPIRATION_DATE', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { format: 'invalid' }); @@ -1211,7 +1211,7 @@ describe('Collect container', () => { it('test valid option for EXPIRATION_DATE', () => { const validFormat = 'YYYY/MM' - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { format: validFormat }); @@ -1222,7 +1222,7 @@ describe('Collect container', () => { }); it('test enableCardIcon option is enabled for elements', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCardIcon: true }); @@ -1234,7 +1234,7 @@ describe('Collect container', () => { }); it('test enableCopy option is enabled for elements', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCopy: true }); @@ -1246,7 +1246,7 @@ describe('Collect container', () => { }); it('test enableCardIcon option is disabled for elements', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCardIcon: false }); @@ -1257,7 +1257,7 @@ describe('Collect container', () => { }); it('test enableCopy option is disabled for elements', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCopy: false }); @@ -1270,7 +1270,7 @@ describe('Collect container', () => { it('test invalid option for EXPIRATION_YEAR', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationYearElement, { format: 'invalid' }); @@ -1282,7 +1282,7 @@ describe('Collect container', () => { it('test valid option for EXPIRATION_YEAR', () => { const validFormat = 'YYYY' - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationYearElement, { format: validFormat }); @@ -1293,13 +1293,13 @@ describe('Collect container', () => { }); it("container collect", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); container.collect().then().catch(err => { expect(err).toBeDefined(); }) }); it("container create options", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryDate = container.create({ table: 'pii_fields', column: 'primary_card.cvv', @@ -1316,7 +1316,7 @@ describe('Collect container', () => { }); }); it("container create options 2", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryDate = container.create({ table: 'pii_fields', column: 'primary_card.cvv', @@ -1334,7 +1334,7 @@ describe('Collect container', () => { }); it('create valid file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let file; try { file = container.create(FileElement); @@ -1346,7 +1346,7 @@ describe('Collect container', () => { }); it('skyflowID undefined for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1358,7 +1358,7 @@ describe('Collect container', () => { } }); it('empty table for Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ column: 'col', @@ -1370,7 +1370,7 @@ describe('Collect container', () => { } }); it('invalid table for Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ column: 'col', @@ -1383,7 +1383,7 @@ describe('Collect container', () => { } }); it('invalid table for Element case 2', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ column: 'col', @@ -1396,7 +1396,7 @@ describe('Collect container', () => { } }); it('missing column for Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1408,7 +1408,7 @@ describe('Collect container', () => { } }); it('invalid column for Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1421,7 +1421,7 @@ describe('Collect container', () => { } }); it('invalid column for Element case 2', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1434,7 +1434,7 @@ describe('Collect container', () => { } }); it('invalid column for Element case 2', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1446,7 +1446,7 @@ describe('Collect container', () => { } }); it('skyflowID is missing for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1456,7 +1456,7 @@ describe('Collect container', () => { } }); it('skyflowID empty for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1468,7 +1468,7 @@ describe('Collect container', () => { } }); it('skyflowID null for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1480,7 +1480,7 @@ describe('Collect container', () => { } }); it('skyflowID of invalid type for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1492,7 +1492,7 @@ describe('Collect container', () => { } }); it('skyflowID of invalid type for file Element another case', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1504,7 +1504,7 @@ describe('Collect container', () => { } }); it('skyflowID undefined for collect Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1516,7 +1516,7 @@ describe('Collect container', () => { } }); it('skyflowID empty for collect Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1528,7 +1528,7 @@ describe('Collect container', () => { } }); it('skyflowID null for collect Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1540,7 +1540,7 @@ describe('Collect container', () => { } }); it('skyflowID of invalid type for collect Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1552,7 +1552,7 @@ describe('Collect container', () => { } }); it('skyflowID null for collect Element another case', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1565,7 +1565,7 @@ describe('Collect container', () => { }); it("container collect options", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const options = { tokens: true, additionalFields: { @@ -1594,7 +1594,7 @@ describe('Collect container', () => { }) }); it("container collect options error case 2", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const element1 = container.create(cvvElement2); const options = { tokens: true, @@ -1622,7 +1622,7 @@ describe('Collect container', () => { const div1 = document.createElement('div'); const div2 = document.createElement('div'); - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); element1.mount(div1); @@ -1642,7 +1642,7 @@ describe('Collect container', () => { }); it("container collect options error", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const options = { tokens: true, additionalFields: { @@ -1671,12 +1671,12 @@ describe('iframe cleanup logic', () => { let container; let div1; let div2; - let emitSpy; + let emitSpy; let targetSpy; let onSpy; beforeEach(() => { - emitSpy = null; + emitSpy = null; targetSpy = null; onSpy = null; emitSpy = jest.spyOn(bus, 'emit'); @@ -1698,7 +1698,7 @@ describe('iframe cleanup logic', () => { it('should remove unmounted iframe elements', () => { // Create and mount elements - container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); @@ -1729,7 +1729,7 @@ describe('iframe cleanup logic', () => { }); it('should handle empty document.body', () => { - container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const element1 = container.create(cvvElement); element1.mount(div1); @@ -1759,8 +1759,9 @@ describe('iframe cleanup logic', () => { expect(error).not.toBeDefined(); }); }); - it('should remove unmounted iframe elements', () => { - container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + + it('should remove unmounted iframe elements', () => { + container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); // Create and mount elements const element1 = container.create(cvvElement); diff --git a/tests/core/external/collect/collect-container.test.ts b/tests/core/external/collect/collect-container.test.ts index d17ae55c..fd778506 100644 --- a/tests/core/external/collect/collect-container.test.ts +++ b/tests/core/external/collect/collect-container.test.ts @@ -1,10 +1,16 @@ /* Copyright (c) 2025 Skyflow, Inc. */ -import { ElementType } from "../../../../src/core/constants"; +import { + ELEMENT_EVENTS_TO_IFRAME, + ElementType, +} from "../../../../src/core/constants"; import CollectContainer from "../../../../src/core/external/collect/collect-container"; import CollectElement from "../../../../src/core/external/collect/collect-element"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import { Metadata } from "../../../../src/core/internal/internal-types"; import * as iframerUtils from "../../../../src/iframe-libs/iframer"; +import { ContainerType } from "../../../../src/skyflow"; import { LogLevel, Env, @@ -36,47 +42,33 @@ jest.mock("../../../../src/libs/uuid", () => ({ default: jest.fn(() => mockUuid), })); -const metaData = { +const metaData: Metadata = { uuid: "123", - skyflowContainer: { - isControllerFrameReady: true, - }, - config: { - vaultID: "vault123", - vaultURL: "https://sb.vault.dev", - getBearerToken, - }, - metaData: { - clientDomain: "http://abc.com", - }, + sdkVersion: "", + sessionId: "1234", + clientDomain: "http://abc.com", + containerType: ContainerType.COLLECT, clientJSON: { config: { vaultID: "vault123", vaultURL: "https://sb.vault.dev", getBearerToken, }, + metaData: { + uuid: "123", + clientDomain: "http://abc.com", + }, }, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, }; -const metaData2 = { - uuid: "123", + +const metaData2: Metadata = { + ...metaData, skyflowContainer: { isControllerFrameReady: false, - }, - config: { - vaultID: "vault123", - vaultURL: "https://sb.vault.dev", - getBearerToken, - }, - metaData: { - clientDomain: "http://abc.com", - }, - clientJSON: { - config: { - vaultID: "vault123", - vaultURL: "https://sb.vault.dev", - getBearerToken, - }, - }, + } as unknown as SkyflowContainer, }; const collectStylesOptions = { @@ -89,7 +81,7 @@ const collectStylesOptions = { } as InputStyles, }; -const cvvElement: CollectElementInput = { +const cvvInput: CollectElementInput = { table: "pii_fields", column: "primary_card.cvv", placeholder: "cvv", @@ -98,20 +90,20 @@ const cvvElement: CollectElementInput = { ...collectStylesOptions, }; -const cardNumberElement: CollectElementInput = { +const cardNumberInput: CollectElementInput = { table: "pii_fields", column: "primary_card.card_number", type: ElementType.CARD_NUMBER, ...collectStylesOptions, }; -const ExpirationDateElement: CollectElementInput = { +const ExpirationDateInput: CollectElementInput = { table: "pii_fields", column: "primary_card.expiry", type: ElementType.EXPIRATION_DATE, }; -const FileElement: CollectElementInput = { +const fileInput: CollectElementInput = { table: "pii_fields", column: "primary_card.file", type: ElementType.FILE_INPUT, @@ -154,31 +146,28 @@ describe("Collect container", () => { document.body.innerHTML = ""; }); - it("container collect success", () => { - let collectContainer = new CollectContainer( - {}, - metaData, - {}, - { logLevel: LogLevel.ERROR, env: Env.PROD } - ); - const div1 = document.createElement("div"); - const div2 = document.createElement("div"); + it("should successfully collect data from elements", () => { + const collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const div1 = document.createElement("div1"); + const div2 = document.createElement("div2"); - const element1: CollectElement = collectContainer.create(cvvElement); - const element2: CollectElement = collectContainer.create(cardNumberElement); + const element1: CollectElement = collectContainer.create(cvvInput); + const element2: CollectElement = collectContainer.create(cardNumberInput); element1.mount(div1); element2.mount(div2); const mountCvvCb = onSpy.mock.calls[2][1]; - mountCvvCb({ - name: `element:${cvvElement.type}:${btoa(element1.getID())}`, + name: `element:${cvvInput.type}:${btoa(element1.getID())}`, }); const mountCardNumberCb = onSpy.mock.calls[5][1]; mountCardNumberCb({ - name: `element:${cardNumberElement.type}:${btoa(element2.getID())}`, + name: `element:${cardNumberInput.type}:${btoa(element2.getID())}`, }); collectContainer @@ -199,53 +188,188 @@ describe("Collect container", () => { }); it("should successfully upload files when elements are mounted", async () => { - const container = new CollectContainer( - {}, - metaData, - {}, - { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + const collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); const div = document.createElement("div"); - const fileElement = container.create(FileElement); + const fileElement = collectContainer.create(fileInput); fileElement.mount(div); const mountCb = onSpy.mock.calls[2][1]; mountCb({ - name: `element:${FileElement.type}:${btoa(fileElement.getID())}`, + name: `element:${fileInput.type}:${btoa(fileElement.getID())}`, }); - const uploadPromise: Promise = container.uploadFiles({ - tokens: true, - }); + const uploadPromise: Promise = + collectContainer.uploadFiles(); - const uploadRequestCb = emitSpy.mock.calls[1][2]; + const uploadFileCallRequestEvent = emitSpy.mock.calls.find((call) => { + return ( + call[0] && + call[0].includes(ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS) + ); + }); + expect(uploadFileCallRequestEvent).toBeDefined(); + const uploadRequestCb = uploadFileCallRequestEvent[2]; uploadRequestCb({ - fileUploadResonse: [{ skyflow_id: "1234" }], + fileUploadResonse: [{ skyflow_id: "abc-def" }], }); const expectedResponse = await uploadPromise; - console.log(JSON.stringify(expectedResponse, null, 2)) - + console.log(JSON.stringify(expectedResponse, null, 2)); expect(expectedResponse).toBeDefined(); }); it("tests different collect element options for elements", () => { - const container = new CollectContainer( - {}, - metaData, - {}, - { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + const collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); let expiryElement: CollectElement; let elementOptions: CollectElementOptions = { required: true, enableCardIcon: true, enableCopy: true, }; - expiryElement = container.create(ExpirationDateElement, elementOptions); + expiryElement = collectContainer.create( + ExpirationDateInput, + elementOptions + ); const options = expiryElement.getOptions(); expect(options.enableCardIcon).toBe(true); expect(options.enableCopy).toBe(true); }); }); + +describe("iframe cleanup logic", () => { + let collectContainer: CollectContainer; + let div1: HTMLElement; + let div2: HTMLElement; + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let onSpy: jest.SpyInstance; + + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + onSpy = jest.spyOn(bus, "on"); + targetSpy.mockReturnValue({ + on, + off: jest.fn(), + }); + div1 = document.createElement("div"); + div2 = document.createElement("div"); + document.body.innerHTML = ""; + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + document.body.innerHTML = ""; + }); + + it("should remove unmounted iframe elements", () => { + // Create and mount elements + collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + const element1 = collectContainer.create(cvvInput); + const element2 = collectContainer.create(cardNumberInput); + + element1.mount(div1); + element2.mount(div2); + + const mountCvvCb = onSpy.mock.calls[2][1]; + mountCvvCb({ + name: `element:${cvvInput.type}:${btoa(element1.getID())}`, + }); + + const mountCardNumberCb = onSpy.mock.calls[5][1]; + mountCardNumberCb({ + name: `element:${cardNumberInput.type}:${btoa(element2.getID())}`, + }); + + // Mock iframe elements in document + const iframe1 = document.createElement("iframe"); + iframe1.id = element1.iframeName(); + document.body.appendChild(iframe1); + + // Trigger cleanup by calling collect + collectContainer.collect().catch((error) => { + expect(error).not.toBeDefined(); + }); + }); + + it("should handle empty document.body", () => { + collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + const element1 = collectContainer.create(cvvInput); + element1.mount(div1); + + const mountCvvCb = onSpy.mock.calls[2][1]; + mountCvvCb({ + name: `element:${cvvInput.type}:${btoa(element1.getID())}`, + }); + + // Mock document.body as null + const originalBody = document.body; + Object.defineProperty(document, "body", { + value: null, + writable: true, + }); + + collectContainer.collect().catch(() => {}); + + // Restore document.body + Object.defineProperty(document, "body", { + value: originalBody, + writable: true, + }); + + // Elements should remain unchanged + collectContainer.collect().catch((error) => { + expect(error).not.toBeDefined(); + }); + }); + + it("should remove unmounted iframe elements", () => { + collectContainer = new CollectContainer(metaData2, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + // Create and mount elements + const element1 = collectContainer.create(cvvInput); + const element2 = collectContainer.create(cardNumberInput); + + element1.mount(div1); + element2.mount(div2); + + const mountCvvCb = onSpy.mock.calls[2][1]; + mountCvvCb({ + name: `element:${cvvInput.type}:${btoa(element1.getID())}`, + }); + + const mountCardNumberCb = onSpy.mock.calls[5][1]; + mountCardNumberCb({ + name: `element:${cardNumberInput.type}:${btoa(element2.getID())}`, + }); + + // Mock iframe elements in document + const iframe1 = document.createElement("iframe"); + iframe1.id = element1.iframeName(); + document.body.appendChild(iframe1); + + // Trigger cleanup by calling collect + collectContainer.collect().catch((error) => { + expect(error).not.toBeDefined(); + }); + }); +}); diff --git a/tests/core/external/collect/collect-element.test.ts b/tests/core/external/collect/collect-element.test.ts new file mode 100644 index 00000000..a070cf91 --- /dev/null +++ b/tests/core/external/collect/collect-element.test.ts @@ -0,0 +1,1359 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import bus from "framebus"; +import CollectElement from "../../../../src/core/external/collect/collect-element"; +import SkyflowError from "../../../../src/libs/skyflow-error"; +import { + LogLevel, + Env, + ValidationRuleType, + CollectElementInput, + LabelStyles, + ErrorTextStyles, + ElementState, +} from "../../../../src/utils/common"; +import { + ELEMENT_EVENTS_TO_CLIENT, + ELEMENT_EVENTS_TO_IFRAME, + ElementType, +} from "../../../../src/core/constants"; +import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import { ContainerType } from "../../../../src/skyflow"; +import EventEmitter from "../../../../src/event-emitter"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import { Metadata } from "../../../../src/core/internal/internal-types"; + +global.ResizeObserver = jest.fn(() => ({ + observe: jest.fn(), + disconnect: jest.fn(), + unobserve: jest.fn(), +})); + +const elementName = "element:CVV:cGlpX2ZpZWxkcy5wcmltYXJ5X2NhcmQuY3Z2"; +const id = "id"; +const input: CollectElementInput = { + table: "pii_fields", + column: "primary_card.cvv", + inputStyles: { + base: { + color: "#1d1d1d", + }, + }, + placeholder: "cvv", + label: "cvv", + type: ElementType.CVV, +}; + +const composableElementName = + "element:group:YXJ5X2NhcmQuY3Z2cGlpX2ZpZWxkcy5wcmlt"; + +const composableInput: CollectElementInput = { + table: "pii_fields", + column: "primary_card.card_numner", + inputStyles: { + base: { + color: "#1d1d1d", + }, + }, + placeholder: "XXXX XXXX XXXX XXXX", + label: "card number", + type: ElementType.CARD_NUMBER, +}; + +const labelStyles: LabelStyles = { + base: { + fontSize: "16px", + fontWeight: "bold", + }, +}; + +const errorTextStyles: ErrorTextStyles = { + base: { + color: "#f44336", + }, +}; + +const rows = [ + { + elements: [ + { + elementName, + elementType: input.type, + name: input.column, + labelStyles, + errorTextStyles, + ...input, + }, + ], + }, +]; + +const composableRows = [ + { + elements: [ + { + composableElementName, + elementType: input.type, + elementName, + name: input.column, + labelStyles, + errorTextStyles, + ...input, + }, + { + composableElementName, + elementType: composableInput.type, + name: composableInput.column, + labelStyles, + errorTextStyles, + ...composableInput, + }, + ], + }, +]; + +const updateElementInput = { + elementType: ElementType.CVV, + name: input.column, + ...input, +}; + +const destroyCallback = jest.fn(); +const updateCallback = jest.fn(); +const groupEmittFn = jest.fn(); +let groupOnCb: Function; +const groupEmiitter: EventEmitter = { + _emit: groupEmittFn, + on: jest.fn().mockImplementation((_, cb) => { + groupOnCb = cb; + }), + events: {}, + off: jest.fn(), + hasListener: jest.fn(), + resetEvents: jest.fn(), +}; + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); +const metaData: Metadata = { + uuid: "123", + sdkVersion: "", + sessionId: "1234", + clientDomain: "http://abc.com", + containerType: ContainerType.COLLECT, + clientJSON: { + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev", + getBearerToken, + }, + metaData: { + uuid: "123", + clientDomain: "http://abc.com", + }, + }, + skyflowContainer: { + isFrameControllerReady: true, + } as unknown as SkyflowContainer, +}; + +jest.mock("../../../../src/event-emitter"); +let emitterSpy: Function; + +(EventEmitter as unknown as jest.Mock).mockImplementation(() => ({ + on: jest.fn().mockImplementation((_, cb) => { + emitterSpy = cb; + }), + _emit: jest.fn(), +})); + +const on = jest.fn(); +describe("testing collect element under various scenarios", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + off: jest.fn(), + }); + }); + + it("tests constructor for collect element", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { elementName, rows }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.FOCUS, + value: {}, + }, + cb2 + ); + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.BLUR, + value: {}, + }, + cb2 + ); + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + value: {}, + }, + cb2 + ); + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(() => { + inputCb( + { + name: elementName, + event: "Invalid event", + }, + cb2 + ); + }).toThrow(SkyflowError); + + element.updateElement({ table: "table", elementName: "element" }); + + expect(element.elementType).toBe(input.type); + expect(element.isMounted()).toBe(false); + expect(element.isValidElement()).toBe(true); + + const heightCb = emitSpy.mock.calls[1][2]; + heightCb({ + height: "123", + }); + }); + + it("tests constructor for collect element with element mounted", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputEvent = onSpy.mock.calls[1][0]; + expect(inputEvent).toBe(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName); + const inputCb = onSpy.mock.calls[1][1]; + const cb2 = jest.fn(); + + const mountEvent = onSpy.mock.calls[2][0]; + expect(mountEvent).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName); + const mountCb = onSpy.mock.calls[2][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + + mountCb( + { + name: elementName, + }, + cb3 + ); + + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + cb3(); + const heightCb = emitSpy.mock.calls[0][2]; + heightCb({ + name: elementName, + height: "123", + }); + }); + + it("tests constructor with element mounted for different element", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputCb = onSpy.mock.calls[1][1]; + const inputEvent = onSpy.mock.calls[1][0]; + expect(inputEvent).toBe(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName); + const cb2 = jest.fn(); + + const mountedEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName + ); + const mountCb = mountedEvent[0][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + + mountCb( + { + name: elementName, + }, + cb3 + ); + + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + cb3(); + const heightCb = emitSpy.mock.calls[0][2]; + heightCb({ + height: "123", + }); + }); + + it("tests constructor with composable elements", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows: composableRows, + }, + metaData, + { + type: ContainerType.COMPOSABLE, + containerId: "containerId", + isMounted: true, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.FOCUS, + value: {}, + }, + cb2 + ); + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.BLUR, + value: {}, + }, + cb2 + ); + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + value: {}, + }, + cb2 + ); + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.elementType).toBe(input.type); + expect(element.isMounted()).toBe(false); + expect(element.isValidElement()).toBe(true); + expect(element.getID()).toBe(id); + + cb2(); + }); + + it("tests constructor with composable elements mounted", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows: composableRows, + }, + metaData, + { + type: ContainerType.COMPOSABLE, + containerId: "containerId", + isMounted: true, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + const mountedEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName + ); + const mountCb = mountedEvent[0][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + + mountCb( + { + name: elementName, + }, + cb3 + ); + + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + cb3(); + + const heightCb = emitSpy.mock.calls[0][2]; + heightCb({ + name: elementName, + height: "123", + }); + }); + + it("tests mount collect element for invalid dom element", () => { + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + expect(() => { + element.mount("#123"); + }).not.toThrow(SkyflowError); + }); + + it("tests mount collect element after container mount for valid dom element", () => { + const onSpy = jest.spyOn(bus, "on"); + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const div = document.createElement("div"); + + expect(element.isMounted()).toBe(false); + + element.mount(div); + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + element.updateElementGroup(updateElementInput); + element.unmount(); + }); + + it("tests mount composable element for valid dom element", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows: composableRows, + }, + metaData, + { + type: ContainerType.COMPOSABLE, + containerId: "containerId", + isMounted: true, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const div = document.createElement("div"); + + expect(element.isMounted()).toBe(false); + + // groupOnCb({containerId:'containerId'}); + element.mount(div); + // const frameReayEvent = onSpy.mock.calls + // .filter((data) => data[0] === `${ELEMENT_EVENTS_TO_IFRAME.FRAME_READY}containerId`); + // const frameReadyCb = frameReayEvent[0][1]; + // const cb2 = jest.fn(); + // frameReadyCb({ + // name: `${elementName}:containerId` + `:ERROR:${btoa(clientDomain)}`, + // }, cb2); + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + element.updateElementGroup(updateElementInput); + element.unmount(); + }); + + it("tests mount collect element before conatiner mount for valid dom element", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: true, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const div = document.createElement("div"); + + expect(element.isMounted()).toBe(false); + + element.mount(div); + // const frameReayEvent = onSpy.mock.calls + // .filter((data) => data[0] === `${ELEMENT_EVENTS_TO_IFRAME.FRAME_READY}containerId`); + // const frameReadyCb = frameReayEvent[0][1]; + // const cb2 = jest.fn(); + // frameReadyCb({ + // name: `${elementName}:containerId` + ':ERROR', + // }, cb2); + // groupOnCb({containerId:'containerId'}); + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + element.updateElementGroup(updateElementInput); + element.unmount(); + }); + + it("tests mount collect element before conatiner mount for valid dom element with isMounted false", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const div = document.createElement("div"); + + expect(element.isMounted()).toBe(false); + + element.mount(div); + + // groupOnCb({containerId:'containerId'}); + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + element.update(updateElementInput); + element.unmount(); + }); + + it("should update element properties when element is mounted", () => { + const onSpy = jest.spyOn(bus, "on"); + const element = new CollectElement( + id, + { elementName, rows }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + const mountedEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName + ); + const mountCb = mountedEvent[0][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + mountCb({ name: elementName }, cb3); + expect(element.isMounted()).toBe(true); + element.update({ label: "Henry" }); + }); + + it("should update element properties when element is not mounted", () => { + const element = new CollectElement( + id, + { elementName, rows }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + expect(element.isMounted()).toBe(false); + expect(element.isUpdateCalled()).toBe(false); + element.update({ label: "Henry" }); + emitterSpy(); + expect(element.isMounted()).toBe(true); + }); + + it("should update element group", () => { + const onSpy = jest.spyOn(bus, "on"); + const element = new CollectElement( + id, + { elementName, rows }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + const mountedEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName + ); + const mountCb = mountedEvent[0][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + mountCb({ name: elementName }, cb3); + expect(element.isMounted()).toBe(true); + element.updateElementGroup({ elementName, rows }); + }); +}); + +const row = { + elementName, + elementType: "CVV", + name: input.column, + labelStyles, + errorTextStyles, + ...input, +}; + +describe("testing collect element validations", () => { + it("Invalid ElementType", () => { + const invalidElementType = [ + { + elements: [ + { + ...row, + elementType: "inValidElementType", + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidElementType, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ELEMENT_TYPE, [], true) + ); + }); + + it("Invalid validations type", () => { + const invalidValidations = [ + { + elements: [ + { + ...row, + validations: "", + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidations, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_VALIDATIONS_TYPE, [], true) + ); + }); + + it("Empty validations rule", () => { + const invalidValidationRule = [ + { + elements: [ + { + ...row, + validations: [{}], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidationRule, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.MISSING_VALIDATION_RULE_TYPE, + [0], + true + ) + ); + }); + + it("Invalid validations RuleType", () => { + const invalidValidationRule = [ + { + elements: [ + { + ...row, + validations: [ + { + type: "Invalid Rule", + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidationRule, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.INVALID_VALIDATION_RULE_TYPE, + [0], + true + ) + ); + }); + + it("Missing params in validations Rule", () => { + const invalidValidationRule = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidationRule, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.MISSING_VALIDATION_RULE_PARAMS, + [0], + true + ) + ); + }); + + // above tests in this block are not necessary for typescript ideally. + + it("should throw error for invalid params in validations Rule", () => { + const invalidValidationRule = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + params: "", + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidationRule, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.INVALID_VALIDATION_RULE_PARAMS, + [0], + true + ) + ); + }); + + it("should throw error for missing regex param in REGEX_MATCH_RULE", () => { + const invalidParams = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.REGEX_MATCH_RULE, + params: { + error: "Regex match failed", + }, + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidParams, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.MISSING_REGEX_IN_REGEX_MATCH_RULE, + [0], + true + ) + ); + }); + + it("should throw error for missing min,max params in LENGTH_MATCH_RULE", () => { + const invalidParams = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + params: { + error: "length match failed", + }, + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidParams, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.MISSING_MIN_AND_MAX_IN_LENGTH_MATCH_RULE, + [0], + true + ) + ); + }); + + it("should throw error for missing element param in ELEMENT_VALUE_MATCH_RULE", () => { + const invalidParams = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, + params: { + error: "length match failed", + }, + }, + ], + }, + ], + }, + ]; + + try { + const element = new CollectElement( + id, + { + elementName, + rows: invalidParams, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + } catch (err) { + expect(err).toBeUndefined(); + } + }); +}); + +describe("testing collect element methods", () => { + // const emitSpy = jest.spyOn(bus, "emit"); + // const onSpy = jest.spyOn(bus, "on"); + const testCollectElementProd = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testCollectElementDev = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.DEV } + ); + + it("tests valid on listener return state in handler for element in DEV env", () => { + let handlerState; + const handler = (state: ElementState) => { + handlerState = state; + }; + const mockState = { + name: "cardnumberiframe", + isEmpty: false, + isValid: false, + isFocused: true, + value: "4111", + elementType: "CARD_NUMBER", + isRequired: true, + selectedCardScheme: "", + isComplete: false, + }; + testCollectElementDev.on("CHANGE", handler); + emitterSpy(mockState); + expect(handlerState).toEqual(mockState); + }); + + it("tests valid on listener return state in handler for element in PROD env", () => { + let handlerState; + const handler = (state: ElementState) => { + handlerState = state; + }; + const mockState = { + name: "cardnumberiframe", + isEmpty: false, + isValid: false, + isFocused: true, + value: undefined, + elementType: "CVV", + isRequired: true, + selectedCardScheme: "", + isComplete: false, + }; + testCollectElementProd.on("CHANGE", handler); + emitterSpy(mockState); + expect(handlerState).toEqual(mockState); + }); + + it("should create a ResizeObserver when mounted", () => { + const testCollectElementProd = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + let div = document.createElement("div"); + div.setAttribute("id", "id1"); + testCollectElementProd.mount(div); + + expect(ResizeObserver).toHaveBeenCalled(); + expect(testCollectElementProd.resizeObserver?.observe).toHaveBeenCalledWith( + div + ); + div.style.display = "none"; + expect(ResizeObserver).toHaveBeenCalled(); + expect(testCollectElementProd.resizeObserver?.observe).toHaveBeenCalledWith( + div + ); + testCollectElementProd.unmount(); + expect(ResizeObserver).toHaveBeenCalled(); + expect( + testCollectElementProd.resizeObserver?.disconnect + ).toHaveBeenCalled(); + }); + + it("ResizeObserver should get disconnect when unmounted", () => { + const testCollectElementProd = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + let div = document.createElement("div"); + div.setAttribute("id", "id1"); + document.body.appendChild(div); + testCollectElementProd.mount("#id1"); + + expect(ResizeObserver).toHaveBeenCalled(); + expect(testCollectElementProd.resizeObserver?.observe).toHaveBeenCalledWith( + document.querySelector("#id1") + ); + + testCollectElementProd.unmount(); + expect(ResizeObserver).toHaveBeenCalled(); + expect( + testCollectElementProd.resizeObserver?.disconnect + ).toHaveBeenCalled(); + }); +}); diff --git a/tests/core/external/collect/composable-container.test.js b/tests/core/external/collect/composable-container.test.js index 4dd7be88..c77561f2 100644 --- a/tests/core/external/collect/composable-container.test.js +++ b/tests/core/external/collect/composable-container.test.js @@ -187,17 +187,17 @@ describe('test composable container class',()=>{ it('test constructor', () => { - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); expect(container).toBeInstanceOf(ComposableContainer); }); it('test create method',()=>{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); expect(element).toBeInstanceOf(ComposableElement); }); it('should throw error when create method is called with no element',(done)=>{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); container.collect().catch((err) => { done(); expect(err).toBeDefined(); @@ -209,7 +209,7 @@ describe('test composable container class',()=>{ }) it('should throw error when create method is called with no element case 2',(done)=>{ - const container = new ComposableContainer({layout:[1]}, metaData2, {}, context); + const container = new ComposableContainer(metaData2, {}, context, {layout:[1]}); container.collect().catch((err) => { done(); expect(err).toBeDefined(); @@ -221,7 +221,7 @@ describe('test composable container class',()=>{ }) it('test create method with callback',()=>{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); // on.mock.calls[0][1]({name : "collect_controller1234"},()=>{}); // on.mock.calls[1][1]({name : "collect_controller"},()=>{}); @@ -232,7 +232,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2]}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -250,10 +250,10 @@ describe('test composable container class',()=>{ document.body.append(div); const container = new ComposableContainer( - { layout: [2], styles: { base: { width: '100px' } } }, metaData, {}, - context + context, + { layout: [2], styles: { base: { width: '100px' } } } ); const element1 = container.create(cvvElement); @@ -306,7 +306,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -330,7 +330,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -352,7 +352,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -389,7 +389,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData2, {}, context); + const container = new ComposableContainer(metaData2, {}, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -411,7 +411,7 @@ describe('test composable container class',()=>{ div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData2, {}, context); + const container = new ComposableContainer(metaData2, {}, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); @@ -431,7 +431,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); try{ @@ -448,7 +448,8 @@ describe('test composable container class',()=>{ }); it("test container collect", () => { - let container = new ComposableContainer({layout:[2],styles:{base:{width:'100px'}},errorTextStyles:{base:{color:'red'}}}, metaData, {}, context); + const containerOptions = {layout:[2],styles:{base:{width:'100px'}},errorTextStyles:{base:{color:'red'}}}; + let container = new ComposableContainer(metaData, [], context, containerOptions); // const div = document.createElement('div'); // div.id = 'composable' // document.body.append(div); @@ -488,7 +489,7 @@ describe('test composable container class',()=>{ div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2]}); // const frameReadyCb = on.mock.calls[0][1]; // const cb2 = jest.fn(); // frameReadyCb({ @@ -506,7 +507,7 @@ describe('test composable container class',()=>{ it('test on method without parameters will throw error',()=>{ try{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]},); const element = container.create(cvvElement); container.on(); expect(element).toBeInstanceOf(ComposableElement); @@ -517,7 +518,7 @@ describe('test composable container class',()=>{ it('test on method without event name will throw error',()=>{ try { - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); container.on("CHANGE"); expect(element).toBeInstanceOf(ComposableElement); @@ -528,7 +529,7 @@ describe('test composable container class',()=>{ it('test on method passing handler as invalid type will throw error',()=>{ try { - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); container.on("CHANGE","test"); expect(element).toBeInstanceOf(ComposableElement); @@ -538,7 +539,7 @@ describe('test composable container class',()=>{ }); it('test on method without error',()=>{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); container.on("CHANGE",()=>{}); expect(element).toBeInstanceOf(ComposableElement); diff --git a/tests/core/external/collect/composable-container.test.ts b/tests/core/external/collect/composable-container.test.ts new file mode 100644 index 00000000..94eee38d --- /dev/null +++ b/tests/core/external/collect/composable-container.test.ts @@ -0,0 +1,453 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import { + ELEMENT_EVENTS_TO_IFRAME, + ElementType, +} from "../../../../src/core/constants"; +import { + LogLevel, + Env, + ValidationRuleType, + CollectElementInput, + CollectResponse, + Context, + ICollectOptions, +} from "../../../../src/utils/common"; +import ComposableContainer from "../../../../src/core/external/collect/compose-collect-container"; +import ComposableElement from "../../../../src/core/external/collect/compose-collect-element"; +import CollectElement from "../../../../src/core/external/collect/collect-element"; +import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import EventEmitter from "../../../../src/event-emitter"; +import { parameterizedString } from "../../../../src/utils/logs-helper"; +import SkyflowError from "../../../../src/libs/skyflow-error"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import { ContainerType } from "../../../../src/skyflow"; +import { Metadata } from "../../../../src/core/internal/internal-types"; + +const bus = require("framebus"); + +jest.mock("../../../../src/iframe-libs/iframer", () => { + const actualModule = jest.requireActual( + "../../../../src/iframe-libs/iframer" + ); + const mockedModule = { ...actualModule }; + mockedModule.__esModule = true; + mockedModule.getIframeSrc = jest.fn(() => "https://google.com"); + return mockedModule; +}); + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); + +const mockUuid = "1234"; +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const mockUnmount = jest.fn(); +const updateMock = jest.fn(); +jest.mock("../../../../src/core/external/collect/collect-element"); + +(CollectElement as unknown as jest.Mock).mockImplementation( + (_, tempElements) => { + tempElements.rows[0].elements.forEach((element) => { + element.isMounted = true; + }); + return { + isMounted: () => true, + mount: jest.fn(), + isValidElement: () => true, + unmount: mockUnmount, + updateElement: updateMock, + }; + } +); + +jest.mock("../../../../src/event-emitter"); +const emitMock = jest.fn(); + +let emitterSpy: Function; +let composableUpdateSpy: Function; +(EventEmitter as unknown as jest.Mock).mockImplementation(() => ({ + on: jest.fn().mockImplementation((name, cb) => { + if (name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_UPDATE_OPTIONS) { + composableUpdateSpy = cb; + } + emitterSpy = cb; + }), + _emit: emitMock, +})); + +const metaData: Metadata = { + uuid: "123", + sdkVersion: "", + sessionId: "1234", + clientDomain: "http://abc.com", + containerType: ContainerType.COMPOSABLE, + clientJSON: { + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev", + getBearerToken, + }, + metaData: { + uuid: "123", + clientDomain: "http://abc.com", + }, + }, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, +}; + +const metaData2: Metadata = { + ...metaData, + skyflowContainer: { + isControllerFrameReady: false, + } as unknown as SkyflowContainer, +}; + +const collectStylesOptions = { + inputStyles: { + cardIcon: { + position: "absolute", + left: "8px", + top: "calc(50% - 10px)", + }, + }, +}; + +const cvvElementInput: CollectElementInput = { + table: "pii_fields", + column: "primary_card.cvv", + placeholder: "cvv", + label: "cvv", + type: ElementType.CVV, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min: 2, + max: 4, + error: "Error", + }, + }, + ], + ...collectStylesOptions, +}; + +const cardNumberElement: CollectElementInput = { + table: "pii_fields", + column: "primary_card.card_number", + type: ElementType.CARD_NUMBER, + ...collectStylesOptions, +}; + +const context: Context = { logLevel: LogLevel.ERROR, env: Env.PROD }; +const on = jest.fn(); + +const collectResponse: CollectResponse = { + records: [ + { + table: "table", + fields: { + first_name: "token1", + primary_card: { + card_number: "token2", + cvv: "token3", + }, + }, + }, + ], +}; + +describe("test composable container class", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let onSpy: jest.SpyInstance; + let eventEmitterSpy: jest.SpyInstance; + + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + eventEmitterSpy = jest.spyOn(EventEmitter.prototype, "on"); + onSpy = jest.spyOn(bus, "on"); + targetSpy.mockReturnValue({ + on, + off: jest.fn(), + }); + }); + + it("tests constructor", () => { + const container = new ComposableContainer(metaData, [], context, { + layout: [1], + }); + expect(container).toBeInstanceOf(ComposableContainer); + }); + + it("tests create method", () => { + const container = new ComposableContainer(metaData, [], context, { + layout: [1], + }); + const element = container.create(cvvElementInput); + expect(element).toBeInstanceOf(ComposableElement); + }); + + it("should throw error when create method is called with no element", (done) => { + const container = new ComposableContainer(metaData, [], context, { + layout: [1], + }); + container.collect().catch((err) => { + done(); + expect(err).toBeDefined(); + expect(err).toBeInstanceOf(SkyflowError); + expect(err.error.code).toBe( + SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE.code + ); + expect(err.error.description).toBe( + parameterizedString( + SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE.description + ) + ); + }); + }); + + it("should throw error when create method is called with no element case 2", (done) => { + const container = new ComposableContainer(metaData2, [], context, { + layout: [1], + }); + container.collect().catch((err) => { + done(); + expect(err).toBeDefined(); + expect(err).toBeInstanceOf(SkyflowError); + expect(err.error.code).toBe( + SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE.code + ); + expect(err.error.description).toBe( + parameterizedString( + SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE.description + ) + ); + }); + }); + + it("test create method with callback", () => { + const container = new ComposableContainer(metaData, [], context, { + layout: [1], + }); + const element = container.create(cvvElementInput); + expect(element).toBeInstanceOf(ComposableElement); + }); + + it("tests mount", () => { + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + }); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + emitterSpy(); + container.mount("#composable"); + }); + + it("tests collect with success and error scenarios", async () => { + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + styles: { base: { width: "100px" } }, + }); + + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + + container.mount("#composable"); + + const options: ICollectOptions = { + tokens: true, + additionalFields: { + records: [ + { + table: "string", + fields: { + column1: "value", + }, + }, + ], + }, + upsert: [ + { + table: "table", + column: "column", + }, + ], + }; + + const collectPromiseSuccess: Promise = + container.collect(options); + + const collectCb1 = emitSpy.mock.calls[0][2]; + collectCb1(collectResponse); + + const successResult = await collectPromiseSuccess; + expect(successResult).toEqual(collectResponse); + + const collectPromiseError: Promise = + container.collect(options); + const collectCb2 = emitSpy.mock.calls[1][2]; + collectCb2({ error: "Error occurred" }); + + await expect(collectPromiseError).rejects.toEqual("Error occurred"); + }); + + it("tests collect when isMount is false", async () => { + let readyCb: Function; + on.mockImplementation((_, cb) => { + readyCb = cb; + }); + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + styles: { base: { width: "100px" } }, + }); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + emitterSpy(); + + container.mount("#composable"); + Object.defineProperty(container, "#isMounted", { + value: false, + writable: true, + }); + + container.collect(); + + on.mockImplementation((_, cb) => { + emitterSpy = cb; + }); + }); + + it("tests updateListeners function", () => { + let readyCb: Function; + on.mockImplementation((_, cb) => { + readyCb = cb; + }); + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + + const container = new ComposableContainer(metaData2, [], context, { + layout: [2], + styles: { base: { width: "100px" } }, + }); + + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + emitterSpy(); + composableUpdateSpy({ elementName: "element:CARD_NUMBER:MTIzNA==" }); + + container.mount("#composable"); + container.collect(); + + on.mockImplementation((name, cb) => { + emitterSpy = cb; + }); + }); + + it("tests collect without mounting the container", (done) => { + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + styles: { base: { width: "100px" } }, + }); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + try { + container + .collect() + .then((res) => { + done(res); + }) + .catch((err) => { + expect(err.error.description).toBe( + parameterizedString( + SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED.description + ) + ); + done(); + }); + } catch (err) { + done(err); + } + }); + + it("tests container collect", () => { + const containerOptions = { + layout: [2], + styles: { base: { width: "100px" } }, + errorTextStyles: { base: { color: "red" } }, + }; + let container = new ComposableContainer( + metaData, + [], + context, + containerOptions + ); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + container.mount("#composable"); + + const options: ICollectOptions = { + tokens: true, + additionalFields: { + records: [ + { + table: "string", //table into which record should be inserted + fields: { + column1: "value", + }, + }, + ], + }, + upsert: [ + { + table: "table", + column: "column", + }, + ], + }; + emitterSpy(); + setTimeout(() => { + container.collect(options); + const collectCb = emitSpy.mock.calls[0][2]; + collectCb(collectResponse); + collectCb({ error: "Error occured" }); + }, 200); + }); + + it("test container unmount", () => { + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + }); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + setTimeout(() => { + container.mount("#composable"); + container.unmount(); + expect(mockUnmount).toBeCalled(); + }, 0); + }); +}); diff --git a/tests/core/external/collect/composable-element.test.ts b/tests/core/external/collect/composable-element.test.ts new file mode 100644 index 00000000..9ce17816 --- /dev/null +++ b/tests/core/external/collect/composable-element.test.ts @@ -0,0 +1,111 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import ComposableElement from "../../../../src/core/external/collect/compose-collect-element"; +import EventEmitter from "../../../../src/event-emitter"; +import { ContainerType } from "../../../../src/skyflow"; +import { ElementState } from "../../../../src/utils/common"; + +describe("test composable element", () => { + const emitter = jest.fn(); + let emitSpy: Function; + const testEventEmitter: EventEmitter = { + _emit: emitter, + on: (name: string, cb: Function) => { + if (name.includes("FOCUS")) { + cb({ + isValid: true, + isComplete: true, + name: "element", + value: undefined, + }); + } else if (name.includes("testce2")) { + emitSpy = cb; + } else { + cb({ + isValid: true, + isComplete: true, + name: "element", + value: "", + }); + } + }, + off: jest.fn(), + events: {}, + hasListener: jest.fn(), + resetEvents: jest.fn(), + }; + + const handler = jest.fn(); + const iframeName = "controller_iframe"; + const testElement = new ComposableElement( + "testce1", + testEventEmitter, + iframeName + ); + const testElement2 = new ComposableElement( + "testce2", + testEventEmitter, + iframeName + ); + const testElement3 = new ComposableElement( + "testce3", + testEventEmitter, + iframeName + ); + + it("tests for iframe name to be correct", () => { + expect(testElement3.type).toBe(ContainerType.COMPOSABLE); + const iframe = testElement3.iframeName(); + expect(iframe).toBe(iframeName); + }); + + it("tests for element name to be correct", () => { + expect(testElement3.type).toBe(ContainerType.COMPOSABLE); + const id = testElement3.getID(); + expect(id).toBe("testce3"); + }); + + it("tests valid CHANGE listener on composable element", () => { + expect(testElement.type).toBe(ContainerType.COMPOSABLE); + testElement.on("CHANGE", handler); + expect(handler).toBeCalledWith({ value: "", isValid: true }); + }); + + it("tests valid FOCUS listener on composable element", () => { + expect(testElement.type).toBe(ContainerType.COMPOSABLE); + testElement.on("FOCUS", handler); + expect(handler).toBeCalledWith({ value: "", isValid: true }); + }); + + it("tests invalid listener on composable element", () => { + try { + testElement.on("invalid_listener", (_: ElementState) => {}); + } catch (err) { + expect(err).toBeDefined(); + } + }); + + it("should update element propeties when element is mounted", () => { + const testUpdateOptions = { table: "table" }; + testElement.update(testUpdateOptions); + expect(emitter).toBeCalledWith("COMPOSABLE_UPDATE_OPTIONS", { + elementName: "testce1", + elementOptions: testUpdateOptions, + }); + }); + + it("should update element propeties when element is not mounted", () => { + const testUpdateOptions = { table: "table" }; + testElement2.update(testUpdateOptions); + expect(emitter).not.toBeCalledWith("COMPOSABLE_UPDATE_OPTIONS", { + elementName: "testce2", + elementOptions: testUpdateOptions, + }); + emitSpy(); + expect(emitter).toBeCalledWith("COMPOSABLE_UPDATE_OPTIONS", { + elementName: "testce2", + elementOptions: testUpdateOptions, + }); + }); +}); diff --git a/tests/core/external/reveal/reveal-container.test.ts b/tests/core/external/reveal/reveal-container.test.ts new file mode 100644 index 00000000..59f39777 --- /dev/null +++ b/tests/core/external/reveal/reveal-container.test.ts @@ -0,0 +1,437 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import RevealContainer from "../../../../src/core/external/reveal/reveal-container"; +import { + ELEMENT_EVENTS_TO_CLIENT, + ELEMENT_EVENTS_TO_CONTAINER, + ELEMENT_EVENTS_TO_IFRAME, + REVEAL_FRAME_CONTROLLER, + REVEAL_TYPES, +} from "../../../../src/core/constants"; +import bus from "framebus"; +import { LogLevel, Env } from "../../../../src/utils/common"; +import RevealElement from "../../../../src/core/external/reveal/reveal-element"; +import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import { parameterizedString } from "../../../../src/utils/logs-helper"; +import SkyflowError from "../../../../src/libs/skyflow-error"; +import logs from "../../../../src/utils/logs"; +import { Metadata } from "../../../../src/core/internal/internal-types"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import { ContainerType, RevealResponse } from "../../../../src/index-node"; +import { ISkyflow } from "../../../../src/skyflow"; +import assert, { AssertionError, fail } from "assert"; + +jest.mock("../../../../src/iframe-libs/iframer", () => { + const actualModule = jest.requireActual( + "../../../../src/iframe-libs/iframer" + ); + const mockedModule = { ...actualModule }; + mockedModule.__esModule = true; + mockedModule.getIframeSrc = jest.fn(() => "https://google.com"); + return mockedModule; +}); + +const mockUuid = "1234"; +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const on = jest.fn(); +const off = jest.fn(); +jest.setTimeout(40000); + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); +const testMetaData: Metadata = { + uuid: "123", + sdkVersion: "", + sessionId: "1234", + clientDomain: "http://abc.com", + containerType: ContainerType.REVEAL, + clientJSON: { + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev", + getBearerToken, + }, + metaData: { + uuid: "123", + clientDomain: "http://abc.com", + }, + }, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, +}; + +const testMetaData2: Metadata = { + ...testMetaData, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, +}; + +const testRecord = { + token: "1677f7bd-c087-4645-b7da-80a6fd1a81a4", + label: "", + styles: { + base: { + color: "#32ce21", + }, + }, +}; + +const testRevealContainer1 = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, +}); + +const testRevealContainer2 = new RevealContainer(testMetaData2, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, +}); + +describe("Reveal Container Class", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let onSpy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + onSpy = jest.spyOn(bus, "on"); + targetSpy.mockReturnValue({ + on, + off, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test("reveal should throw error with no elements", (done) => { + const container = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + container.reveal().catch((error) => { + done(); + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(SkyflowError); + expect(error.error.code).toEqual(400); + expect(error.error.description).toEqual( + logs.errorLogs.NO_ELEMENTS_IN_REVEAL + ); + }); + }); + + test("constructor", () => { + expect(testRevealContainer1).toBeInstanceOf(RevealContainer); + expect(testRevealContainer1).toBeInstanceOf(Object); + expect(testRevealContainer1).toHaveProperty("create"); + expect(testRevealContainer1).toHaveProperty("reveal"); + expect(testRevealContainer1).toHaveProperty("type"); + }); + + test("create() will return a Reveal Element", () => { + const testRevealElement = testRevealContainer1.create(testRecord); + expect(testRevealElement).toBeInstanceOf(RevealElement); + }); + + test("create() will throw error if record id invalid", () => { + try { + testRevealContainer1.create({ token: "" }); + } catch (error) { + expect(error.message).toBe("Invalid Token Id "); + } + }); + + test("create() will throw error for invalid input format options", (done) => { + try { + testRevealContainer1.create({ token: "1244" }, { format: undefined }); + done("should throw error"); + } catch (error) { + expect(error.error.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_INPUT_OPTIONS_FORMAT.description + ) + ); + done(); + } + }); + + test("handle reveal errors with 404 response", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + element.mount(div); + + // First emit the mounted event + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ error: { code: 404, description: "Not Found" } }); + } + }); + + // Trigger the mounted callback + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + // Now try to reveal + await expect(testRevealContainer.reveal()).rejects.toEqual({ + code: 404, + description: "Not Found", + }); + }); + + test("handle successful reveal when called before mounting", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + + // Setup emit spy to handle reveal request + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ success: [{ token: "1815-6223-1073-1425" }] }); + } + }); + + const revealPromise = testRevealContainer.reveal(); + + // Mount and trigger mounted event + element.mount(div); + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + const response = await revealPromise; + expect(response.success).toBeDefined(); + expect(response.success![0].token).toBe("1815-6223-1073-1425"); + }); + + test("frame controller ready event correctly", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + element.mount(div); + + // Mock frame controller ready event + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ success: [{ token: "1815-6223-1073-1425" }] }); + } + }); + + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + const response = await testRevealContainer.reveal(); + expect(response.success).toBeDefined(); + expect(response.success![0].token).toBe("1815-6223-1073-1425"); + }); + + test("on container mounted else call back", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + element.mount(div); + + // Mock error response + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ error: { code: 404, description: "Not Found" } }); + } + }); + + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + await expect(testRevealContainer.reveal()).rejects.toEqual({ + code: 404, + description: "Not Found", + }); + }); + + test("on container mounted else call back 1", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + element.mount(div); + + // Mock successful response + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ success: [{ token: "1815-6223-1073-1425" }] }); + } + }); + + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + const response = await testRevealContainer.reveal(); + expect(response.success).toBeDefined(); + expect(response.success![0].token).toBe("1815-6223-1073-1425"); + }); + + test("reveal before skyflow frame ready event", async () => { + const testRevealContainer = new RevealContainer(testMetaData2, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + // Create reveal element with test token + const testToken = "1815-6223-1073-1425"; + const div = document.createElement("div"); + const revealElement = testRevealContainer.create({ + token: testToken, + }); + + // Mount the element + revealElement.mount(div); + + // Mock success response data + const successResponse: RevealResponse = { + success: [ + { + token: testToken, + valueType: "string", + }, + ], + }; + + // Setup event listeners and callbacks before calling reveal + const elementMountedEvent = + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + mockUuid; + const revealRequestEvent = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + testMetaData2.uuid; + + // Handle element mounted event + bus.emit(elementMountedEvent, { + token: testToken, + containerId: mockUuid, + }); + + // Get and execute the mounted callback + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(elementMountedEvent); + const onCb = on.mock.calls[0][1]; + onCb({ token: testToken, containerId: mockUuid }); + + // Call reveal and await response + const revealPromise = testRevealContainer.reveal(); + + // Get and execute the reveal request callback + const emitEventName = emitSpy.mock.calls[1][0]; + const emitData = emitSpy.mock.calls[1][1]; + const emitCb = emitSpy.mock.calls[1][2]; + + expect(emitEventName).toBe(revealRequestEvent); + expect(emitData).toEqual({ + type: REVEAL_TYPES.REVEAL, + containerId: mockUuid, + records: [{ token: testToken }], + }); + + // Simulate successful reveal response + emitCb(successResponse); + + // Verify the final response + const response = await revealPromise; + expect(response).toEqual(successResponse); + expect(response.success).toBeDefined(); + expect(response.success![0].token).toBe(testToken); + expect(response.success![0].valueType).toBe("string"); + }); + + test("reveal before skyflow frame ready when element have error", (done) => { + var element = testRevealContainer2.create({ token: "1815-6223-1073-1425" }); + element.setError("error occ"); + + testRevealContainer2.reveal().catch((error: RevealResponse) => { + done(); + expect(error).toBeDefined(); + expect(error.errors).toBeDefined(); + expect(error.errors![0].error.code).toEqual(400); + expect(error.errors![0].error.description).toEqual( + logs.errorLogs.REVEAL_ELEMENT_ERROR_STATE + ); + }); + }); + + test("reveal before skyflow frame ready", (done) => { + var element = testRevealContainer1.create({ token: "1815-6223-1073-1425" }); + element.setError("error occ"); + + testRevealContainer1.reveal().catch((error: RevealResponse) => { + done(); + expect(error).toBeDefined(); + expect(error.errors).toBeDefined(); + expect(error.errors![0].error.code).toEqual(400); + expect(error.errors![0].error.description).toEqual( + logs.errorLogs.REVEAL_ELEMENT_ERROR_STATE + ); + }); + }); + + test("reveal when elment is empty when skyflow ready", (done) => { + testRevealContainer2.reveal().catch((error: RevealResponse) => { + done(); + expect(error).toBeDefined(); + expect(error.errors).toBeDefined(); + expect(error.errors![0].error.code).toEqual(400); + expect(error.errors![0].error.description).toEqual( + logs.errorLogs.NO_ELEMENTS_IN_REVEAL + ); + }); + }); + + test("reveal when element is empty when skyflow frame not ready", (done) => { + testRevealContainer1.reveal().catch((error: RevealResponse) => { + done(); + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(SkyflowError); + expect(error.errors).toBeDefined(); + expect(error.errors![0].error.code).toEqual(400); + expect(error.errors![0].error.description).toEqual( + logs.errorLogs.NO_ELEMENTS_IN_REVEAL + ); + }); + }); +}); diff --git a/tests/core/external/reveal/reveal-element.test.ts b/tests/core/external/reveal/reveal-element.test.ts new file mode 100644 index 00000000..35ecdb71 --- /dev/null +++ b/tests/core/external/reveal/reveal-element.test.ts @@ -0,0 +1,1019 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import { LogLevel, Env } from "../../../../src/utils/common"; +import { + ELEMENT_EVENTS_TO_IFRAME, + FRAME_REVEAL, + ELEMENT_EVENTS_TO_CLIENT, + REVEAL_TYPES, + REVEAL_ELEMENT_OPTIONS_TYPES, +} from "../../../../src/core/constants"; +import RevealElement from "../../../../src/core/external/reveal/reveal-element"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import Client from "../../../../src/client"; + +import * as busEvents from "../../../../src/utils/bus-events"; + +import bus from "framebus"; +import { JSDOM } from "jsdom"; +import { Metadata } from "../../../../src/core/internal/internal-types"; +import { ContainerType, ISkyflow } from "../../../../src/skyflow"; +import { IRevealElementInput } from "../../../../src/core/external/reveal/reveal-container"; +import EventEmitter from "../../../../src/event-emitter"; +import { RevealElementInput } from "../../../../src/index-node"; + +jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject("access token")); + +const mockUuid = "1234"; +const elementId = "id"; +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const getBearerToken = jest.fn(); +const getBearerTokenReject = jest + .fn() + .mockImplementation(() => Promise.reject()); +const groupEmittFn = jest.fn(); + +let groupOnCb: Function; +const groupEmiitter: EventEmitter = { + _emit: groupEmittFn, + on: jest.fn().mockImplementation((args, cb) => { + groupOnCb = cb; + }), + events: {}, + off: jest.fn(), + hasListener: jest.fn(), + resetEvents: jest.fn(), +}; + +jest.mock("../../../../src/libs/jss-styles", () => { + return { + __esModule: true, + default: jest.fn(), + generateCssWithoutClass: jest.fn(), + getCssClassesFromJss: jest.fn().mockReturnValue({ + base: { color: "red" }, + global: { backgroundColor: "black" }, + }), + }; +}); + +jest.mock("../../../../src/core/external/skyflow-container", () => { + return { + __esModule: true, + default: jest.fn(), + }; +}); + +const skyflowConfig: ISkyflow = { + vaultID: "e20afc3ae1b54f0199f24130e51e0c11", + vaultURL: "https://testurl.com", + getBearerToken: jest.fn(), + options: { trackMetrics: true, trackingKey: "key" }, +}; +const clientDomain = "http://abc.com"; +const metaData: Metadata = { + uuid: "123", + clientDomain: clientDomain, + sdkVersion: "", + sessionId: "1234", + containerType: ContainerType.REVEAL, + clientJSON: { + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev", + getBearerToken: getBearerToken, + }, + metaData: { + uuid: "123", + clientDomain: clientDomain, + }, + }, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, +}; + +const metaData2: Metadata = { + ...metaData, + skyflowContainer: { + isControllerFrameReady: false, + } as unknown as SkyflowContainer, +}; + +// const clientData = { +// uuid: "123", +// client: { +// config: { ...skyflowConfig }, +// metadata: { uuid: "123", skyflowContainer: controller }, +// }, +// clientJSON: { +// context: { logLevel: LogLevel.ERROR, env: Env.PROD }, +// config: { +// ...skyflowConfig, +// getBearerToken: jest.fn().toString(), +// }, +// }, +// skyflowContainer: { +// isControllerFrameReady: true, +// }, +// clientDomain: clientDomain, +// }; + +// const clientData2: Metadata = { +// uuid: "123", +// clientDomain: clientDomain, +// client: { +// config: { ...skyflowConfig }, +// metadata: { +// uuid: "123", +// skyflowContainer: { +// isControllerFrameReady: true, +// } as unknown as SkyflowContainer +// }, +// }, +// clientJSON: { +// context: { logLevel: LogLevel.ERROR, env: Env.PROD }, +// config: { +// ...skyflowConfig, +// getBearerToken: jest.fn(), +// }, +// }, +// skyflowContainer: { +// isControllerFrameReady: false, +// } as unknown as SkyflowContainer, +// }; + +// const client: Client = new Client(clientData.client.config, clientData); + +// let controller = new SkyflowContainer(client, { +// logLevel: LogLevel.DEBUG, +// env: Env.DEV, +// }); + +const testRecord: IRevealElementInput = { + token: "1677f7bd-c087-4645-b7da-80a6fd1a81a4", +}; +const on = jest.fn(); +const off = jest.fn(); +let skyflowContainer: SkyflowContainer; + +describe("Reveal Element Class", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + off, + }); + const client = new Client(skyflowConfig, metaData); + skyflowContainer = new SkyflowContainer(client, { + logLevel: LogLevel.DEBUG, + env: Env.PROD, + }); + }); + + const containerId = mockUuid; + + test("Constructor for reveal element", () => { + const testRevealElement = new RevealElement( + testRecord, + undefined, + metaData, + { + containerId: containerId, + isMounted: false, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + expect(testRevealElement).toBeInstanceOf(RevealElement); + }); + + test("Mount method for reveal element", () => { + const testRevealElement = new RevealElement( + testRecord, + undefined, + metaData, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + + expect(document.getElementById("testDiv")).not.toBeNull(); + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + expect(document.querySelector("iframe")).toBeTruthy(); + + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + const eventListenerName = ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testIframeName; + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(eventListenerName); + + const onCb = on.mock.calls[0][1]; + onCb({ name: testIframeName }); + + expect(testRevealElement.isMounted()).toBe(true); + expect(testRevealElement.iframeName()).toBe(testIframeName); + expect(testRevealElement.hasToken()).toBe(true); + }); + + test("Mount method for file render element", () => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + undefined, + metaData, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + + expect(document.getElementById("testDiv")).not.toBeNull(); + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + const eventListenerName = ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testIframeName; + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(eventListenerName); + + const onCb = on.mock.calls[0][1]; + onCb({ name: testIframeName }); + + expect(testRevealElement.isMounted()).toBe(true); + expect(testRevealElement.iframeName()).toBe(testIframeName); + }); + + test("file render success scenario", () => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + {}, + metaData2, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + const eventListenerName = ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testIframeName; + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(eventListenerName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testIframeName, + }); + + expect(testRevealElement.isMounted()).toBe(true); + expect(testRevealElement.iframeName()).toBe(testIframeName); + + testRevealElement + .renderFile() + .then((data) => + expect(data).toEqual({ + success: { skyflow_id: "1244", column: "column" }, + }) + ) + .catch((error) => console.log("error", error)); + const frameReadyEvent = on.mock.calls[1][0]; + expect(frameReadyEvent).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + "123" + ); + const onCallback = on.mock.calls[1][1]; + const cb = jest.fn(); + onCallback({}, cb); + expect(emitSpy.mock.calls[3][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + "123" + ); + expect(emitSpy.mock.calls[3][1]).toEqual({ + type: REVEAL_TYPES.RENDER_FILE, + records: { + altText: "alt text", + skyflowID: "1244", + column: "column", + table: "table", + }, + containerId: mockUuid, + iframeName: testIframeName, + }); + const emitCb = emitSpy.mock.calls[3][2]; + emitCb({ success: { skyflow_id: "1244", column: "column" } }); + }); + + test("renderFile when SKYFLOW_FRAME_CONTROLLER_READY is not triggered success case", (done) => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + undefined, + metaData2, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + // Call renderFile before triggering SKYFLOW_FRAME_CONTROLLER_READY + const renderPromise = testRevealElement.renderFile(); + + // Verify that the else block is executed + const frameReadyEventName = + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + "123"; + const onCbName = on.mock.calls[1][0]; + expect(onCbName).toBe(frameReadyEventName); + + // Simulate the SKYFLOW_FRAME_CONTROLLER_READY event + const onCb = on.mock.calls[1][1]; + const cb = jest.fn(); + onCb({}, cb); + + const emitCb = emitSpy.mock.calls[0][2]; + emitCb({ success: { skyflow_id: "1244", column: "column" } }); + + // Verify the renderFile promise resolves correctly + renderPromise + .then((data) => { + expect(data).toEqual({}); + done(); + }) + .catch((error) => { + console.error("Error:", error); + done(error); + }); + }); + + test("renderFile when SKYFLOW_FRAME_CONTROLLER_READY is not triggered error case", (done) => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + undefined, + metaData, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + // Call renderFile before triggering SKYFLOW_FRAME_CONTROLLER_READY + const renderPromise = testRevealElement.renderFile(); + + const emitCb = emitSpy.mock.calls[0][2]; + emitCb({ + errors: { + column: "column", + skyflowId: "1244", + error: { code: 400, description: "No Records Found" }, + }, + }); + + // Verify the renderFile promise resolves correctly + renderPromise.catch((error) => { + expect(error).toEqual({ + errors: { + column: "column", + skyflowId: "1244", + error: { + code: 400, + description: "No Records Found", + }, + }, + }); + done(); + }); + }); + + test("file render error case", () => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + undefined, + metaData, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + const eventListenerName = ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testIframeName; + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(eventListenerName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testIframeName, + }); + expect(testRevealElement.isMounted()).toBe(true); + expect(testRevealElement.iframeName()).toBe(testIframeName); + testRevealElement + .renderFile() + .then((data) => console.log("data", data)) + .catch((error) => { + expect(error).toEqual({ + errors: { + grpc_code: 5, + http_code: 404, + message: "No Records Found", + http_status: "Not Found", + details: [], + }, + }); + }); + + expect(emitSpy.mock.calls[3][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + "123" + ); + expect(emitSpy.mock.calls[3][1]).toEqual({ + type: REVEAL_TYPES.RENDER_FILE, + records: { + altText: "alt text", + skyflowID: "1244", + column: "column", + table: "table", + }, + containerId: mockUuid, + iframeName: testIframeName, + }); + const emitCb = emitSpy.mock.calls[3][2]; + emitCb({ + errors: { + grpc_code: 5, + http_code: 404, + message: "No Records Found", + http_status: "Not Found", + details: [], + }, + }); + }); + + test("Mount method with ready to mount false", () => { + const testRevealElement = new RevealElement( + testRecord, + undefined, + metaData, + { + containerId: containerId, + isMounted: false, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + expect(testRevealElement.iframeName()).toBe(testIframeName); + expect(testRevealElement.hasToken()).toBe(true); + }); + + test("Mount method with ready to mount false case 2", () => { + const testRevealElement = new RevealElement( + testRecord, + undefined, + metaData, + { + containerId: containerId, + isMounted: false, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + expect(testRevealElement.iframeName()).toBe(testIframeName); + expect(testRevealElement.hasToken()).toBe(true); + }); + + test("has token should return false, without token", () => { + const testRevealElement = new RevealElement( + {}, + undefined, + metaData, + { + containerId: containerId, + isMounted: false, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + expect(testRevealElement.hasToken()).toBe(false); + }); +}); + +describe("Reveal Element Methods", () => { + const containerId = mockUuid; + const testRevealElement = new RevealElement( + { + token: "1244", + }, + undefined, + metaData, + { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testRevealElement2 = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "demo", + inputStyles: { + base: { + border: "5px solid orange", + padding: "10px 10px", + borderRadius: "10px", + color: "#1d1d1d", + marginTop: "4px", + height: "260px", + width: "400px", + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + }, + errorTextStyles: { + base: { + border: "5px solid orange", + padding: "10px 10px", + borderRadius: "10px", + color: "#1d1d1d", + marginTop: "4px", + height: "260px", + width: "400px", + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + }, + }, + undefined, + metaData, + { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + off, + }); + }); + + test("unmount method", () => { + testRevealElement.unmount(); + testRevealElement2.unmount(); + }); + + test("check for isSetError False", () => { + expect(testRevealElement.isClientSetError()).toBe(false); + }); + + test("setError method", () => { + testRevealElement.mount("#testDiv"); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + testRevealElement.setError("errorText"); + expect(testRevealElement.isClientSetError()).toBe(true); + expect(emitSpy.mock.calls[1][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[1][1]).toEqual({ + name: testRevealElement.iframeName(), + clientErrorText: "errorText", + isTriggerError: true, + }); + expect(emitSpy).toBeCalled(); + }); + + test("when element is not mounted then setError method", () => { + testRevealElement.unmount(); + testRevealElement.setError("errorText"); + + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(testRevealElement.isClientSetError()).toBe(true); + expect(emitSpy).toBeCalled(); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + clientErrorText: "errorText", + isTriggerError: true, + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("setErrorOverride method", () => { + testRevealElement.setErrorOverride("errorText"); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + clientErrorText: "errorText", + isTriggerError: true, + }); + expect(emitSpy).toBeCalled(); + }); + + test("setErrorOverride method when element is not mounted", () => { + testRevealElement.unmount(); + testRevealElement.setErrorOverride("errorText"); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + clientErrorText: "errorText", + isTriggerError: true, + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("check for isSetError True", () => { + expect(testRevealElement.isClientSetError()).toBe(true); + }); + + test("resetError method", () => { + testRevealElement.resetError(); + }); + + test("resetError method when element is not mounted", () => { + testRevealElement.unmount(); + testRevealElement.resetError(); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(testRevealElement.isClientSetError()).toBe(false); + expect(emitSpy).toBeCalled(); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + isTriggerError: false, + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("setAltText method", () => { + testRevealElement.setAltText("altText"); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: "altText", + }); + expect(emitSpy).toBeCalled(); + }); + + test("setAltText method when element is not mounted", () => { + testRevealElement.unmount(); + testRevealElement.setAltText("altText"); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: "altText", + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("clearAltText method", () => { + testRevealElement.clearAltText(); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + expect(emitSpy).toBeCalled(); + }); + + test("clearAltText method when element is not mounted", () => { + testRevealElement.unmount(); + testRevealElement.clearAltText(); + + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("getRecord Data", () => { + const testRecordData = testRevealElement.getRecordData(); + expect(testRecordData).toStrictEqual({ token: "1244" }); + }); + + test("setToken method", () => { + testRevealElement.setToken("testToken"); + }); + + test("setToken method when mount event not happen", () => { + testRevealElement.unmount(); + testRevealElement.setToken("testToken"); + + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + }); + + test("getRecord Data", () => { + const testRecordData = testRevealElement.getRecordData(); + expect(testRecordData).toStrictEqual({ token: "testToken" }); + }); + + test("update the properties of elements when element is mounted", () => { + const { window } = new JSDOM('
'); + document = window._document; + const element = document.createElement("div"); + element.setAttribute("id", "#mockElement"); + testRevealElement2.mount("#mockElement"); + + const testUpdateOptions: RevealElementInput = { + label: "Updated Label", + inputStyles: { + base: { + borderWitdth: "5px", + }, + }, + }; + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement2.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement2.iframeName(), + }); + testRevealElement2.update(testUpdateOptions); + expect(emitSpy.mock.calls[2][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement2.iframeName() + ); + expect(emitSpy.mock.calls[2][1]).toEqual({ + name: testRevealElement2.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: { + label: "Updated Label", + inputStyles: { base: { borderWitdth: "5px" } }, + }, + }); + expect(emitSpy).toBeCalled(); + }); + + test("update the properties of elements when element is unmounted", () => { + testRevealElement2.unmount(); + + const testUpdateOptions: RevealElementInput = { + label: "Updated Label", + inputStyles: { + base: { + borderWitdth: "5px", + }, + }, + }; + testRevealElement2.update(testUpdateOptions); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement2.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement2.iframeName(), + }); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement2.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement2.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: { + label: "Updated Label", + inputStyles: { base: { borderWitdth: "5px" } }, + }, + }); + expect(emitSpy).toBeCalled(); + }); +}); diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js b/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js index 6ae0d80c..99787414 100644 --- a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js +++ b/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js @@ -102,17 +102,14 @@ describe('Uploading files to the vault', () => { }} } })); - const clientReq = jest.fn(() => Promise.resolve({ - fileUploadResponse: [ - {skyflow_id:"file-upload-skyflow-id"} - ] - })); + const clientReq = jest.fn(() => Promise.resolve( + JSON.stringify({ skyflow_id:"file-upload-skyflow-id" }))); jest.spyOn(clientModule, 'fromJSON').mockImplementation(() => ({ ...clientData.client, request: clientReq, toJSON: toJson })); - + SkyflowFrameController.init(); const emitEventName = emitSpy.mock.calls[1][0]; @@ -123,27 +120,27 @@ describe('Uploading files to the vault', () => { const onCb = on.mock.calls[1][1]; const data = { type: COLLECT_TYPES.FILE_UPLOAD, - elementIds: [ - "element:FILE_INPUT:ID" - ], - containerId: "CONTAINER-ID" + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID" }; const cb2 = jest.fn(); onCb(data, cb2); setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + expect(cb2.mock.calls[0][0]).toBeDefined(); expect(cb2.mock.calls[0][0].fileUploadResponse).toBeDefined(); expect(cb2.mock.calls[0][0].fileUploadResponse.length).toBe(1); done(); }, 1000); }); test('should successfully handle FILE_UPLOAD validation', (done) => { - testValue.iFrameFormElement.state.value.name = 'test file.txt'; + testValue.iFrameFormElement.state.value.name = 'test file.txt'; windowSpy.mockImplementation(()=>({ frames:{ 'element:FILE_INPUT:ID:CONTAINER-ID:ERROR:':{document:{ - getElementById:()=>(testValue) + getElementById:()=>(testValue) }} } })); @@ -158,10 +155,8 @@ describe('Uploading files to the vault', () => { const onCb = on.mock.calls[1][1]; const data = { type: COLLECT_TYPES.FILE_UPLOAD, - elementIds: [ - "element:FILE_INPUT:ID" - ], - containerId: "CONTAINER-ID" + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID" }; const cb2 = jest.fn(); @@ -173,7 +168,7 @@ describe('Uploading files to the vault', () => { }, 1000); }); test('should successfully handle FILE_UPLOAD validation case 2', (done) => { - testValue.iFrameFormElement.state.value.name = 'test-file.txt'; + testValue.iFrameFormElement.state.value.name = 'test-file.txt'; testValue.iFrameFormElement.state.value.size = 1024 * 1024 * 32; windowSpy.mockImplementation(()=>({ frames:{ @@ -253,16 +248,16 @@ describe('Uploading files to the vault', () => { windowSpy.mockImplementation(()=>({ frames:{ 'element:FILE_INPUT:ID:CONTAINER-ID:ERROR:':{document:{ - getElementById:()=>(testValue) + getElementById:()=>(testValue) }} } })); - const clientReq = jest.fn(() => Promise.resolve({ + const clientReq = jest.fn(() => Promise.resolve(JSON.stringify({ fileUploadResponse: [ {skyflow_id:"file-upload-skyflow-id"} ], error: "error" - })); + }))); jest.spyOn(clientModule, 'fromJSON').mockImplementation(() => ({ ...clientData.client, request: clientReq, @@ -279,10 +274,8 @@ describe('Uploading files to the vault', () => { const onCb = on.mock.calls[1][1]; const data = { type: COLLECT_TYPES.FILE_UPLOAD, - elementIds: [ - "element:FILE_INPUT:ID" - ], - containerId: "CONTAINER-ID" + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID" }; const cb2 = jest.fn(); @@ -293,6 +286,85 @@ describe('Uploading files to the vault', () => { done(); }, 1000); }); + + test('should handle partial success/error in multiple FILE_UPLOAD attempts', (done) => { + // Mock two file inputs + windowSpy.mockImplementation(() => ({ + frames: { + 'element1:FILE_INPUT:ID:CONTAINER-ID:ERROR:': { + document: { + getElementById: () => (testValue) + } + }, + 'element2:FILE_INPUT:ID:CONTAINER-ID:ERROR:': { + document: { + getElementById: () => ({ + iFrameFormElement: { + ...testValue.iFrameFormElement, + state: { + ...testValue.iFrameFormElement.state, + name: 'file2' + } + } + }) + } + } + } + })); + + // Mock client request to succeed for first file and fail for second + const clientReq = jest.fn((request) => { + if (request.body.get('file2')) { + return Promise.reject({ error: { code: 400, description: "Upload failed" } }); + } else { + return Promise.resolve(JSON.stringify({ skyflow_id: "success-file-id" })); + } + }); + + jest.spyOn(clientModule, 'fromJSON').mockImplementation(() => ({ + ...clientData.client, + request: clientReq, + toJSON: toJson + })); + + SkyflowFrameController.init(); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element1:FILE_INPUT:ID", "element2:FILE_INPUT:ID"], + containerId: "CONTAINER-ID" + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + const result = cb2.mock.calls[0][0]; + + console.log('mock result is\t', result); + + // Should have both success and error responses + expect(result.error.fileUploadResponse).toBeDefined(); + expect(result.error.errorResponse).toBeDefined(); + + // Check successful upload + expect(result.error.fileUploadResponse).toHaveLength(1); + expect(result.error.fileUploadResponse[0].skyflow_id).toBe('success-file-id'); + + // Check failed upload + expect(result.error.errorResponse).toHaveLength(1); + expect(result.error.errorResponse[0].error).toBeDefined(); + + done(); + }, 1000); + }); test('should fail upload files', (done) => { const clientReq = jest.fn(() => Promise.resolve({ fileUploadResponse: [ diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts b/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts new file mode 100644 index 00000000..50bb259d --- /dev/null +++ b/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts @@ -0,0 +1,1874 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import bus from "framebus"; +import { + COLLECT_TYPES, + ELEMENT_EVENTS_TO_IFRAME, +} from "../../../../src/core/constants"; +import clientModule from "../../../../src/client"; +import * as busEvents from "../../../../src/utils/bus-events"; +import { LogLevel, Env, InsertResponse } from "../../../../src/utils/common"; +import SkyflowFrameController from "../../../../src/core/internal/skyflow-frame/skyflow-frame-controller"; +import Client from "../../../../src/client"; +import { ISkyflow } from "../../../../src/skyflow"; +import { + TokenizeDataInput, + UploadFileDataInput, +} from "../../../../src/core/internal/internal-types"; + +jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve("access token")); + +const on = jest.fn(); +const emit = jest.fn(); + +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const mockUuid = "1244"; +const skyflowConfig: ISkyflow = { + vaultID: "e20afc3ae1b54f0199f24130e51e0c11", + vaultURL: "https://testurl.com", + getBearerToken: jest.fn(), +}; + +const clientData = { + client: { + config: { ...skyflowConfig }, + metadata: { + uuid: mockUuid, + clientDomain: "test-domain", + }, + }, + context: { logLevel: LogLevel.ERROR, env: Env.PROD }, +}; + +const toJson = jest.fn(() => ({ + config: {}, + metaData: { + uuid: "", + sdkVersion: "skyflow-react-js@1.2.3", + }, +})); + +describe("Uploading files to the vault", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let windowSpy: jest.SpyInstance; + let testValue: any; + + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + + testValue = { + iFrameFormElement: { + fieldType: "FILE_INPUT", + state: { + value: { + type: "file", + name: "test-file.txt", + size: 1024, + }, + isFocused: false, + isValid: false, + isEmpty: true, + isComplete: false, + name: "test-name", + isRequired: true, + isTouched: false, + selectedCardScheme: "", + }, + tableName: "test-table-name", + preserveFileName: true, + onFocusChange: jest.fn(), + }, + }; + windowSpy = jest.spyOn(window, "parent", "get"); + windowSpy.mockImplementation(() => ({ + frames: {}, + })); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve("access token")); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + jest.restoreAllMocks(); + Object.defineProperty(window.parent, "frames", { + value: undefined, + writable: true, + }); + if (windowSpy) { + windowSpy.mockRestore(); + } + }); + + test("should successfully handle FILE_UPLOAD validation case 1", (done) => { + testValue.iFrameFormElement.state.value.name = "test file.txt"; + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + console.log(cb2.mock.calls[0][0].error.errorResponse[0]); + done(); + }, 1000); + }); + + test("should successfully handle FILE_UPLOAD validation case 2", (done) => { + testValue.iFrameFormElement.state.value.name = "test-file.txt"; + testValue.iFrameFormElement.state.value.size = 1024 * 1024 * 32; + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + console.log(cb2.mock.calls[0][0].error.errorResponse[0]); + done(); + }, 1000); + }); + + test("should successfully handle FILE_UPLOAD validation case 3", (done) => { + testValue.iFrameFormElement.state.value.name = "test-file.txt"; + testValue.iFrameFormElement.state.value.size = 0; + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + const clientReq = jest.fn(); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + console.log(cb2.mock.calls[0][0].error.errorResponse[0]); + done(); + }, 1000); + }); + + test("should successfully handle FILE_UPLOAD event and upload files", (done) => { + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + const clientReq = jest.fn(() => + Promise.resolve(JSON.stringify({ skyflow_id: "file-upload-skyflow-id" })) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + expect(cb2.mock.calls[0][0]).toBeDefined(); + expect(cb2.mock.calls[0][0].fileUploadResponse).toBeDefined(); + expect(cb2.mock.calls[0][0].fileUploadResponse.length).toBe(1); + done(); + }, 1000); + }); + + test("should handle partial success/error in multiple FILE_UPLOAD attempts", (done) => { + windowSpy.mockImplementation(() => ({ + frames: { + "element1:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + "element2:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => ({ + iFrameFormElement: { + ...testValue.iFrameFormElement, + state: { + ...testValue.iFrameFormElement.state, + name: "file2", + }, + }, + }), + }, + }, + }, + })); + + // Mock client request to succeed for first file and fail for second + const clientReq = jest.fn((request) => { + if (request.body.get("file2")) { + return Promise.reject({ + error: { code: 400, description: "Upload failed" }, + }); + } else { + return Promise.resolve( + JSON.stringify({ skyflow_id: "success-file-id" }) + ); + } + }); + + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element1:FILE_INPUT:ID", "element2:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + const result = cb2.mock.calls[0][0]; + + console.log("mock result is\t", result); + + // Should have both success and error responses + expect(result.error.fileUploadResponse).toBeDefined(); + expect(result.error.errorResponse).toBeDefined(); + + // Check successful upload + expect(result.error.fileUploadResponse).toHaveLength(1); + expect(result.error.fileUploadResponse[0].skyflow_id).toBe( + "success-file-id" + ); + + // Check failed upload + expect(result.error.errorResponse).toHaveLength(1); + expect(result.error.errorResponse[0].error).toBeDefined(); + + done(); + }, 1000); + }); + + test("should fail upload files when client rejects promise", (done) => { + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + const clientReq = jest.fn(() => + Promise.reject({ + error: "error", + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + const firstCallArg = cb2.mock.calls[0][0]; + expect(firstCallArg).toBeDefined(); + expect(firstCallArg).toEqual({ + error: { errorResponse: [{ error: "error" }] }, + }); + + done(); + }, 1000); + }); +}); + +describe("SkyflowFrameController - tokenize function", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let windowSpy: jest.SpyInstance; + let testValue: any; + let testValue2: any; + + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + + testValue = { + iFrameFormElement: { + fieldType: "TEXT_INPUT", + state: { + value: "test-value", + isFocused: false, + isValid: true, + isEmpty: false, + isComplete: true, + name: "test-name", + isRequired: true, + isTouched: false, + selectedCardScheme: "", + }, + tableName: "test-table-name", + onFocusChange: jest.fn(), + getUnformattedValue: jest.fn(() => "unformatted-value"), + }, + }; + + testValue2 = { + iFrameFormElement: { + fieldType: "TEXT_INPUT", + state: { + value: "test-value2", + isFocused: false, + isValid: true, + isEmpty: false, + isComplete: true, + name: "test-name2", + isRequired: true, + isTouched: false, + selectedCardScheme: "", + }, + tableName: "test-table-name2", + skyflowID: "id", + onFocusChange: jest.fn(), + getUnformattedValue: jest.fn(() => "unformatted-value2"), + }, + }; + + windowSpy = jest.spyOn(window, "parent", "get"); + windowSpy.mockImplementation(() => ({ + frames: {}, + })); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve("access token")); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + jest.restoreAllMocks(); + Object.defineProperty(window.parent, "frames", { + value: undefined, + writable: true, + }); + if (windowSpy) { + windowSpy.mockRestore(); + } + }); + + test("should tokenize data successfully", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + const insertResponse = { + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }; + const updateError = { + errors: [{ error: { code: 404, description: "Record not found" } }], + }; + + let requestCount = 0; + const clientReq = jest.fn((arg) => { + requestCount++; + if (arg.requestMethod === "PUT") { + return Promise.reject(updateError); + } + if (arg.requestMethod === "POST" && !arg.url.includes("/files")) { + return Promise.resolve(insertResponse); + } + return Promise.resolve(insertResponse); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + console.log("=======================>>>", cb2.mock.calls); + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data successfully case 2", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + const insertResponse = { + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }; + const updateRes = { + records: [ + { + skyflow_id: "id", + fields: { + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }; + + let requestCount = 0; + const clientReq = jest.fn((arg) => { + requestCount++; + if (arg.requestMethod === "PUT") { + return Promise.resolve(updateRes); + } + if (arg.requestMethod === "POST" && !arg.url.includes("/files")) { + return Promise.resolve(insertResponse); + } + return Promise.resolve(insertResponse); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data successfully case 3", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + const insertResponse = { + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }; + const updateRes = { + records: [ + { + skyflow_id: "id", + fields: { + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }; + + let requestCount = 0; + const clientReq = jest.fn((arg) => { + console.log("Request Count:", requestCount, "Arg:", arg); + requestCount++; + if (arg.requestMethod === "PUT") { + return Promise.resolve(updateRes); + } + if (arg.requestMethod === "POST" && !arg.url.includes("/files")) { + return Promise.resolve(insertResponse); + } + return Promise.resolve(insertResponse); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("tokenize data error case when skyflowID is empty", async () => { + testValue2.iFrameFormElement.skyflowID = ""; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].errors).toBeDefined(); + }, 1000); + }); + + test("tokenize data error case when skyflowID is null", async () => { + testValue2.iFrameFormElement.skyflowID = null; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].errors).toBeDefined(); + }, 1000); + }); + + test("tokenize data error case when isValid is false", async () => { + testValue2.iFrameFormElement.skyflowID = "null"; + testValue2.iFrameFormElement.state.isValid = false; + testValue2.iFrameFormElement.state.isRequired = false; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].errors).toBeDefined(); + }, 1000); + }); + + test("should tokenize data accessToken error ", async () => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => + Promise.reject({ error: "reject token error" }) + ); + + testValue2.iFrameFormElement.skyflowID = "dummy-skyflow-id"; + testValue2.iFrameFormElement.state.isValid = true; + testValue2.iFrameFormElement.state.isRequired = false; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].errors).toBeDefined(); + }, 1000); + }); + + test("should tokenize data partial successfully", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data partial successfully case 2", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + const mockResponseBody = { + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }; + let requestCount = 0; + const clientReq = jest.fn((arg) => { + console.log("Request Count:", requestCount, "Arg:", arg); + requestCount++; + if (arg.requestMethod === "POST" && !arg.url.includes("/files")) { + return Promise.resolve(mockResponseBody); + } + }); + + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should handle validations and set value when all conditions are met", async () => { + testValue.iFrameFormElement.validations = [{ rule: "regex", value: ".*" }]; + testValue.iFrameFormElement.state.isValid = true; + testValue.iFrameFormElement.state.isComplete = true; + const setValueMock = jest.fn(); + const onFocusChangeMock = jest.fn(); + testValue.iFrameFormElement.setValue = setValueMock; + testValue.iFrameFormElement.onFocusChange = onFocusChangeMock; + + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + jest + .spyOn( + require("../../../../src/core-utils/collect"), + "checkForElementMatchRule" + ) + .mockReturnValue(true); + jest + .spyOn( + require("../../../../src/core-utils/collect"), + "checkForValueMatch" + ) + .mockReturnValue(true); + + jest + .spyOn( + require("../../../../src/core-utils/collect"), + "constructElementsInsertReq" + ) + .mockImplementation(() => { + return [ + { records: [] }, + { + updateRecords: [ + { + table: "testTable", + fields: { key: "value" }, + skyflowID: "123", + }, + ], + }, + ]; + }); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(setValueMock).toHaveBeenCalledWith( + testValue.iFrameFormElement.state.value + ); + expect(onFocusChangeMock).toHaveBeenCalledWith(false); + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("successful insert and update requests", async () => { + testValue.iFrameFormElement.skyflowID = "test-id"; + windowSpy.mockImplementation(() => ({ + frames: { + "frame1:container123:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + "frame2:container123:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + const insertResponse = { + records: [{ skyflow_id: "inserted-id" }], + }; + const updateResponse = { + tokens: { + card_number: "token123", + cvv: "token456", + }, + }; + + const clientReq = jest.fn((arg) => { + return Promise.resolve(updateResponse); + }); + + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: TokenizeDataInput = { + type: "COLLECT", + elementIds: [ + { frameId: "frame1", elementId: "element1" }, + { frameId: "frame2", elementId: "element2" }, + ], + containerId: "container123", + }; + const cb2 = jest.fn(); + onCb(data, cb2); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBeDefined(); + expect(cb2.mock.calls[0][0].error).toBeUndefined(); + }, 1000); + }); + + test("should successfully tokenize data when fieldType is checkbox", async () => { + testValue.iFrameFormElement.fieldType = "checkbox"; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { + records: [ + { + skyflow_id: "test-id-1", + }, + ], + }, + { + fields: { + "*": "some-random", + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should successfully tokenize data when fieldType is not checkbox", async () => { + testValue.iFrameFormElement.skyflowID = undefined; + testValue.iFrameFormElement.fieldType = "textarea"; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { + records: [ + { + skyflow_id: "test-id-1", + }, + ], + }, + { + fields: { + "*": "some-random", + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should successfully tokenize data when fieldType is not checkbox and validation exist", async () => { + testValue.iFrameFormElement.skyflowID = undefined; + testValue.iFrameFormElement.fieldType = "textarea"; + testValue.iFrameFormElement.validations = [ + { + rule: "regex", + value: ".*", + type: "ELEMENT_VALUE_MATCH_RULE", + }, + ]; + testValue.iFrameFormElement.isMatchEqual = jest.fn(() => true); + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { + records: [ + { + skyflow_id: "test-id-1", + }, + ], + }, + { + fields: { + "*": "some-random", + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should fail tokenize data when doesClientHasError is true", async () => { + testValue.iFrameFormElement.state.isValid = false; + testValue.iFrameFormElement.doesClientHasError = true; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + + const firstArg = cb2.mock.calls[0][0]; + expect(firstArg).toBeDefined(); + expect(firstArg).toHaveProperty("error"); + // done(); + }, 1000); + }); + + test("should fail tokenize data when doesClientHasError is false", async () => { + testValue.iFrameFormElement.state.isValid = false; + testValue.iFrameFormElement.doesClientHasError = false; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + + const firstArg = cb2.mock.calls[0][0]; + expect(firstArg).toBeDefined(); + expect(firstArg).toHaveProperty("error"); + // done(); + }, 1000); + }); + + test("should fail tokenize data when skyflowID is null or empty", async () => { + testValue.iFrameFormElement.state.isValid = false; + testValue.iFrameFormElement.doesClientHasError = false; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + + const firstArg = cb2.mock.calls[0][0]; + expect(firstArg).toBeDefined(); + expect(firstArg).toHaveProperty("error"); + // done(); + }, 1000); + }); + + test("should tokenize data when skyflowID is null or empty and not checkbox", async () => { + testValue.iFrameFormElement.fieldType = "textarea"; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data when skyflowID is null or empty and not checkbox", async () => { + testValue.iFrameFormElement.fieldType = "textarea"; + testValue.iFrameFormElement.skyflowID = "test-skyflow-id"; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data when skyflowID is undefined and not checkbox", async () => { + testValue.iFrameFormElement.fieldType = "textarea"; + testValue.iFrameFormElement.skyflowID = undefined; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); +}); diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts b/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts new file mode 100644 index 00000000..79786435 --- /dev/null +++ b/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts @@ -0,0 +1,1697 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import bus from "framebus"; +import { + ELEMENT_EVENTS_TO_IFRAME, + PUREJS_TYPES, + REVEAL_TYPES, +} from "../../../../src/core/constants"; +import clientModule from "../../../../src/client"; +import * as busEvents from "../../../../src/utils/bus-events"; +import { + LogLevel, + Env, + RedactionType, + IRevealRecord, + IGetRecord, + IGetOptions, + IDeleteRecordInput, +} from "../../../../src/utils/common"; +import SkyflowFrameController from "../../../../src/core/internal/skyflow-frame/skyflow-frame-controller"; +import { InsertOptions } from "../../../../src/index-node"; +import { ISkyflow } from "../../../../src/skyflow"; +import Client from "../../../../src/client"; + +jest.mock("../../../../src/utils/bus-events", () => ({ + ...jest.requireActual("../../../../src/utils/bus-events"), + getAccessToken: jest.fn(() => Promise.resolve("access token")), +})); + +const on = jest.fn(); +const emit = jest.fn(); + +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const mockUuid = "1244"; +const skyflowConfig: ISkyflow = { + vaultID: "e20afc3ae1b54f0199f24130e51e0c11", + vaultURL: "https://testurl.com", + getBearerToken: jest.fn(), +}; + +const clientData = { + client: { + config: { ...skyflowConfig }, + metadata: { + uuid: mockUuid, + }, + }, + context: { logLevel: LogLevel.ERROR, env: Env.PROD }, +}; + +const records = { + records: [ + { + table: "pii_fields", + fields: { + first_name: "Joseph", + primary_card: { + card_number: "4111111111111111", + cvv: "123", + }, + }, + }, + ], +}; + +const options: InsertOptions = { + tokens: true, + upsert: [ + { + table: "", + column: "", + }, + ], +}; + +const pushEventResponse = { + data: 1, +}; + +const insertResponse = { + vaultID: "vault123", + responses: [ + { + table: "table1", + records: [ + { + skyflow_id: "testId", + }, + ], + }, + { + table: "table1", + fields: { + "*": "testId", + first_name: "token1", + primary_card: { + card_number: "token2", + cvv: "token3", + }, + }, + }, + ], +}; + +const insertResponseWithoutTokens = { + vaultID: "vault123", + responses: [ + { + records: [ + { + skyflow_id: "testId", + }, + ], + }, + ], +}; + +const errorResponse = { + error: { + http_code: 403, + message: "RBAC: access denied", + }, +}; + +describe("push event", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let onSpy: jest.SpyInstance; + + beforeEach(() => { + window.name = "controller:frameId:clientDomain:true"; + window.CoralogixRum = { + isInited: true, + init: jest.fn(), + info: jest.fn(), + }; + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + onSpy = jest.spyOn(bus, "on"); + targetSpy.mockReturnValue({ + on, + }); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve("access token")); + }); + + test("before send function in init", (done) => { + const event = { + log_context: { + message: "SDK IFRAME EVENT", + }, + }; + window.CoralogixRum = { + isInited: false, + init: ({ beforeSend }) => { + beforeSend(event); + }, + }; + expect(event).toBeTruthy(); + const clientReq = jest.fn(() => Promise.resolve(pushEventResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + config: { + ...clientData.client.config, + options: { + ...clientData.client?.config?.options, + trackingKey: "aaaaabbbbbcccccdddddeeeeefffffggggg", + }, + }, + toJSON: toJson, + request: clientReq, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = { + event: { + element_id: "element123", + container_id: "container456", + vault_url: "http://example.com", + status: "Error", + events: ["MOUNTED"], + }, + }; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); + + test("init coralogix", (done) => { + const event = { + log_context: { + message: null, + }, + }; + window.CoralogixRum = { + isInited: false, + init: ({ beforeSend }) => { + beforeSend(event); + }, + }; + + const clientReq = jest.fn(() => Promise.resolve(pushEventResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + config: { + ...clientData.client.config, + options: { + ...clientData.client?.config?.options, + trackingKey: "aaaaabbbbbcccccdddddeeeeefffffggggg", + }, + }, + toJSON: toJson, + request: clientReq, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = { + event: { + element_id: "element123", + container_id: "container456", + vault_url: "http://example.com", + status: "Error", + events: ["MOUNTED"], + }, + }; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); + + test("push event with elementid", (done) => { + const clientReq = jest.fn(() => Promise.resolve(pushEventResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = { + event: { + element_id: "element123", + container_id: "container456", + vault_url: "http://example.com", + status: "Error", + events: ["MOUNTED"], + }, + }; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); + + test("push event with error", (done) => { + window.CoralogixRum = { + isInited: false, + init: jest.fn(), + info: jest.fn(), + }; + const clientReq = jest.fn(() => Promise.resolve(pushEventResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = {}; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); + + test("push event throw error resopnse", (done) => { + window.CoralogixRum = { + isInited: false, + init: jest.fn(), + }; + const clientReq = jest.fn(() => Promise.reject(errorResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = { + event: { + element_id: "element123", + container_id: "container456", + vault_url: "http://example.com", + status: "Error", + events: ["MOUNTED"], + }, + }; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); +}); + +describe("Inserting records into the vault", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("insert records with tokens as true", (done) => { + const clientReq = jest.fn(() => Promise.resolve(insertResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.INSERT, + records, + options, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(cb2.mock.calls[0][0].records[0].fields).toBeDefined(); + expect(cb2.mock.calls[0][0].error).toBeUndefined(); + done(); + }, 1000); + }); + + test("insert records with tokens as false", (done) => { + const clientReq = jest.fn(() => + Promise.resolve(insertResponseWithoutTokens) + ); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.INSERT, + records, + options: { tokens: false, upsert: [{ table: "", column: " " }] }, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(cb2.mock.calls[0][0].records[0].fields).toBeUndefined(); + expect(cb2.mock.calls[0][0].error).toBeUndefined(); + done(); + }, 1000); + }); + + test("insert records with error", (done) => { + const clientReq = jest.fn(() => Promise.reject(errorResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.INSERT, + records, + options, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +const detokenizeRecords: IRevealRecord[] = [{ token: "token1" }]; + +const detokenizeRecordWithRedaction: IRevealRecord[] = [ + { + token: "token1", + redaction: RedactionType.MASKED, + }, +]; + +const detokenizeResponse = { + records: [ + { + token_id: "token1", + fields: { + cvv: "123", + }, + }, + ], +}; + +const detokenizeResponseWithRedaction = { + records: [ + { + token_id: "token1", + value: "123", + }, + ], +}; + +const detokenizeErrorResponse = { + error: { + grpc_code: 5, + http_code: 404, + message: "Token not found for token1", + http_status: "Not Found", + details: [], + }, +}; + +const toJson = jest.fn(() => ({ + config: {}, + metaData: { + uuid: "", + sdkVersion: "skyflow-react-js@1.2.3", + }, +})); + +describe("Retrieving data using skyflowId", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("getById success", (done) => { + const clientReq = jest.fn(() => Promise.resolve(getByIdRes)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET_BY_SKYFLOWID, + records: getByIdReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("getById error", (done) => { + const clientReq = jest.fn(() => Promise.reject(errorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET_BY_SKYFLOWID, + records: getByIdReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +describe("Retrieving data using skyflow tokens", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("detokenize success", (done) => { + const clientReq = jest.fn(() => Promise.resolve(detokenizeResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecords, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("detokenize error", (done) => { + const clientReq = jest.fn(() => Promise.reject(detokenizeErrorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecords, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeUndefined(); + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +const getByIdReq: IGetRecord[] = [ + { + ids: ["id1"], + redaction: RedactionType.PLAIN_TEXT, + table: "table1", + }, +]; + +const getByIdReqWithoutRedaction: IGetRecord[] = [ + { + ids: ["id1"], + table: "table1", + }, +]; + +const getOptionsTrue: IGetOptions = { tokens: true }; +const getOptionsFalse: IGetOptions = { tokens: false }; + +const getByColumnReq: IGetRecord[] = [ + { + columnValues: ["id1", "id2", "id3"], + columnName: "column1", + redaction: RedactionType.PLAIN_TEXT, + table: "table1", + }, +]; + +const getByIdRes = { + records: [ + { + fields: { + skyflow_id: "id1", + cvv: "123", + }, + }, + ], +}; + +describe("Retrieving data using skyflow tokens", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("detokenize success", (done) => { + const clientReq = jest.fn(() => + Promise.resolve(detokenizeResponseWithRedaction) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecordWithRedaction, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("detokenize error", (done) => { + const clientReq = jest.fn(() => Promise.reject(detokenizeErrorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecordWithRedaction, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeUndefined(); + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +describe("Retrieving data using get", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("get success", (done) => { + const clientReq = jest.fn(() => Promise.resolve(getByIdRes)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("get success second case", (done) => { + const clientReq = jest.fn(() => Promise.resolve(getByIdRes)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByColumnReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("get success case should have single column_name for multiple column values ", (done) => { + let reqArg; + const clientReq = jest.fn((arg) => { + reqArg = arg; + return Promise.resolve(getByIdRes); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByColumnReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + try { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(reqArg.url.match(/column_name=column1/gi)?.length).toBe(1); + done(); + } catch (err) { + done(err); + } + }, 1000); + }); + + test("get method should send request url with tokenization true and without redaction when tokens flag is true", (done) => { + let reqArg; + const clientReq = jest.fn((arg) => { + reqArg = arg; + return Promise.resolve(getByIdRes); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByIdReqWithoutRedaction, + options: getOptionsTrue, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + try { + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(reqArg.url.includes("tokenization=true")).toBe(true); + expect(reqArg.url.includes("redaction=PLAIN_TEXT")).toBe(false); + done(); + }, 1000); + } catch (err) { + done(err); + } + }); + + test("get method should send request url with tokenization false and redaction when tokens flag is false", (done) => { + let reqArg; + const clientReq = jest.fn((arg) => { + reqArg = arg; + return Promise.resolve(getByIdRes); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + options: getOptionsFalse, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + try { + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(reqArg.url.includes("tokenization=false")).toBe(true); + expect(reqArg.url.includes("redaction=PLAIN_TEXT")).toBe(true); + done(); + }, 1000); + } catch (err) { + done(err); + } + }); + + test("get error", (done) => { + const clientReq = jest.fn(() => Promise.reject(errorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +describe("Failed to fetch accessToken get", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("accessToken error", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + SkyflowFrameController.init(mockUuid); + const onCb = on.mock.calls[0][1]; + + const insertData = { + type: PUREJS_TYPES.INSERT, + records, + options: { tokens: false, upsert: [{ table: "", column: " " }] }, + }; + const insertCb = jest.fn(); + onCb(insertData, insertCb); + + const detokenizeData = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecords, + }; + const detokenizeCb = jest.fn(); + onCb(detokenizeData, detokenizeCb); + + const getByIdData = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + }; + const getByIdCb = jest.fn(); + onCb(getByIdData, getByIdCb); + + setTimeout(() => { + expect(insertCb.mock.calls[0][0].error).toBeDefined(); + expect(detokenizeCb.mock.calls[0][0].error).toBeDefined(); + expect(getByIdCb.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +describe("Failed to fetch accessToken Getbyid", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("accessToken error", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + SkyflowFrameController.init(mockUuid); + const onCb = on.mock.calls[0][1]; + + const insertData = { + type: PUREJS_TYPES.INSERT, + records, + options: { tokens: false, upsert: [{ table: "", column: " " }] }, + }; + const insertCb = jest.fn(); + onCb(insertData, insertCb); + + const detokenizeData = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecords, + }; + const detokenizeCb = jest.fn(); + onCb(detokenizeData, detokenizeCb); + + const getByIdData = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + }; + const getByIdCb = jest.fn(); + onCb(getByIdData, getByIdCb); + + setTimeout(() => { + expect(insertCb.mock.calls[0][0].error).toBeDefined(); + expect(detokenizeCb.mock.calls[0][0].error).toBeDefined(); + expect(getByIdCb.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +const deleteRecords: IDeleteRecordInput = { + records: [ + { + table: "pii_fields", + id: "29ebda8d-5272-4063-af58-15cc674e332b", + }, + { + table: "pii_fields", + id: "29ebda8d-5272-4063-af58-15cc674e332b", + }, + ], +}; + +const deleteOptions = {}; + +const deleteResponse = { + skyflow_id: "29ebda8d-5272-4063-af58-15cc674e332b", + deleted: true, +}; + +const deleteErrorResponse = { + error: { + grpc_code: 5, + http_code: 404, + message: "No Records Found", + http_status: "Not Found", + details: [], + }, +}; + +describe("Deleting records from the vault", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + emit, + }); + }); + + test("delete records success", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve({ token: "token123" })); + const clientReq = jest.fn(() => Promise.resolve(deleteResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DELETE, + records: deleteRecords, + options: deleteOptions, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + try { + expect(cb2.mock.calls[0][0].records.length).toBe(2); + expect(cb2.mock.calls[0][0].records[0].deleted).toBeTruthy(); + expect(cb2.mock.calls[0][0].records[1].deleted).toBeTruthy(); + expect(cb2.mock.calls[0][0].error).toBeUndefined(); + done(); + } catch (err) { + done(err); + } + }, 1000); + }); + + test("delete records with error", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve({ token: "token123" })); + const clientReq = jest.fn(() => Promise.reject(deleteErrorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DELETE, + records: deleteRecords, + options: deleteOptions, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + try { + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + } catch (err) { + done(err); + } + }, 1000); + }); + + test("accessToken error while deleting records", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const deleteData = { + type: PUREJS_TYPES.DELETE, + records: deleteRecords, + options: deleteOptions, + }; + const deleteCb = jest.fn(); + onCb(deleteData, deleteCb); + + setTimeout(() => { + try { + expect(deleteCb.mock.calls[0][0].error).toBeDefined(); + done(); + } catch (err) { + done(err); + } + }, 1000); + }); +}); + +describe("test render file request", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + emit, + }); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + }); + + test("render files error case", () => { + const clientReq = jest.fn(() => + Promise.reject({ + errors: [ + { + skyflowID: "1815-6223-1073-1425", + error: { code: 404, description: "id not found" }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = { + records: [ + { + skyflowID: "1815-6223-1073-1425", + column: "file", + table: "table1", + }, + ], + }; + const data1 = { + type: REVEAL_TYPES.RENDER_FILE, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + setTimeout(() => { + expect(emitterCb.mock.calls[0][0].error).toBeDefined(); + expect(emitterCb.mock.calls[0][0].error).toEqual({ + code: 404, + description: "id not found", + }); + }, 10000); + }); + + test("render files succes case", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + fields: { skyflow_id: "1815-6223-1073-1425", file: "https://demo.com" }, + tokens: null, + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = { + skyflowID: "1815-6223-1073-1425", + column: "file", + table: "table1", + }; + const data1 = { + type: REVEAL_TYPES.RENDER_FILE, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); +}); + +describe("test reveal request", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + emit, + }); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + }); + + test("reveal data error case", () => { + const clientReq = jest.fn(() => + Promise.reject({ + errors: [ + { + token: "1815-6223-1073-1425", + error: { code: 404, description: "token not found" }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = { + records: [ + { + token: "1815-6223-1073-1425", + }, + ], + }; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + setTimeout(() => { + expect(emitterCb.mock.calls[0][0].error).toBeDefined(); + expect(emitterCb.mock.calls[0][0].error).toEqual({ + code: 404, + description: "token not found", + }); + }, 10000); + }); + + test("reveal succes case", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + records: [ + { token: "7402-2242-2342-232", value: "231", valueType: "STRING" }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = [ + { + token: "1815-6223-1073-1425", + }, + ]; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); + + test("reveal data with redaction type", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + records: [ + { token: "7402-2242-2342-232", value: "231", valueType: "STRING" }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = [ + { + token: "1815-6223-1073-1425", + redaction: RedactionType.MASKED, + }, + ]; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); + + test("reveal data with redaction type and without token", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + records: [ + { token: "7402-2242-2342-232", value: "231", valueType: "STRING" }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = [ + { + token: "1815-6223-1073-1425", + redaction: RedactionType.MASKED, + }, + ]; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + options: { tokens: false }, + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); + + test("reveal data with redaction type and without token and without tokenization", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + records: [ + { token: "7402-2242-2342-232", value: "231", valueType: "STRING" }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = [ + { + token: "1815-6223-1073-1425", + redaction: RedactionType.MASKED, + }, + ]; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + options: { tokens: false }, + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); +});