diff --git a/package.json b/package.json index d3839ae5..aefa6708 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0", + "version": "2.5.0-dev.641a405", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", diff --git a/src/core-utils/collect.ts b/src/core-utils/collect.ts index 40039c40..7dd26d37 100644 --- a/src/core-utils/collect.ts +++ b/src/core-utils/collect.ts @@ -242,7 +242,7 @@ const updateRecordsInVault = ( options, ) => { const table = skyflowIdRecord.fields.table; - const skyflowID = skyflowIdRecord.skyflowID; + const skyflowID = skyflowIdRecord?.skyflowID; skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'table'); skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'skyflowID'); return client.request({ @@ -316,6 +316,151 @@ export const updateRecordsBySkyflowID = async ( }); }); +export const updateRecordsBySkyflowIDComposable = async ( + skyflowIdRecords, + client: Client, + options, + authToken: string, +) => new Promise((rootResolve, rootReject) => { + let updateResponseSet: Promise[]; + // eslint-disable-next-line prefer-const + updateResponseSet = skyflowIdRecords?.updateRecords?.map( + (skyflowIdRecord: IInsertRecord) => new Promise((resolve, reject) => { + updateRecordsInVault(skyflowIdRecord, client, authToken, options) + ?.then((resolvedResult: any) => { + const resp = constructFinalUpdateRecordResponse( + resolvedResult, + options?.tokens, + skyflowIdRecord, + ); + resolve(resp); + }, + (rejectedResult) => { + let errorResponse = rejectedResult; + if (rejectedResult?.error) { + errorResponse = { + error: { + code: rejectedResult?.error?.code, + description: rejectedResult?.error?.description, + }, + }; + } + printLog(rejectedResult?.error?.description ?? '', MessageType.ERROR, LogLevel.ERROR); + reject(errorResponse); + })?.catch((error) => { + reject(error); + }); + }), + ); + Promise.allSettled(updateResponseSet)?.then((resultSet: any) => { + const recordsResponse: any[] = []; + const errorsResponse: any[] = []; + resultSet?.forEach((result: { status: string; value: any; reason?: any; }) => { + if (result?.status === 'fulfilled') { + recordsResponse?.push(result?.value); + } else { + errorsResponse?.push(result?.reason); + } + }); + + if (errorsResponse?.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse?.length === 0) { + rootReject({ errors: errorsResponse }); + } else { + rootReject({ records: recordsResponse, errors: errorsResponse }); + } + }); +}); + +export const insertDataInCollect = async ( + records, + client: Client, + options, + finalInsertRecords, + authToken: string, +) => new Promise((resolve) => { + let insertResponse: any; + let insertErrorResponse: any; + client + ?.request({ + body: JSON.stringify({ + records, + }), + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + ?.then((response: any) => { + insertResponse = constructInsertRecordResponse( + response, + options?.tokens, + finalInsertRecords?.records, + ); + resolve(insertResponse); + }) + ?.catch((error) => { + insertErrorResponse = { + errors: [ + { + error: { + code: error?.error?.code, + description: error?.error?.description, + }, + }, + ], + }; + resolve(insertErrorResponse); + }); +}); + +export const insertDataInMultipleFiles = async ( + records, + client: Client, + options, + finalInsertRecords, + authToken: string, +) => new Promise((resolve) => { + let insertResponse: any; + let insertErrorResponse: any; + client + ?.request({ + body: JSON.stringify({ + records, + }), + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + ?.then((response: any) => { + insertResponse = constructInsertRecordResponse( + response, + options?.tokens, + finalInsertRecords?.records, + ); + resolve(insertResponse); + }) + ?.catch((error) => { + insertErrorResponse = { + errors: [ + { + error: { + code: error?.error?.code, + description: error?.error?.description, + }, + }, + ], + }; + resolve(insertErrorResponse); + }); +}); + export const checkForElementMatchRule = (validations: IValidationRule[]) => { if (!validations) return false; for (let i = 0; i < validations.length; i += 1) { diff --git a/src/core-utils/reveal.ts b/src/core-utils/reveal.ts index 7e11fc37..bb334494 100644 --- a/src/core-utils/reveal.ts +++ b/src/core-utils/reveal.ts @@ -16,6 +16,7 @@ import { GetResponseRecord, GetByIdResponse, GetByIdResponseRecord, + IRevealRecordComposable, } from '../utils/common'; import { printLog } from '../utils/logs-helper'; import { FILE_DOWNLOAD_URL_PARAM } from '../core/constants'; @@ -145,7 +146,7 @@ export const getFileURLForRender = ( paramList += `${skyflowIdRecord.skyflowID}?`; - paramList += `fields=${skyflowIdRecord.column}&${FILE_DOWNLOAD_URL_PARAM}`; + paramList += `fields=${skyflowIdRecord.column}&${FILE_DOWNLOAD_URL_PARAM}&returnFileMetadata=true`; const vaultEndPointurl: string = `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${skyflowIdRecord.table}/${paramList}`; return client.request({ @@ -182,6 +183,27 @@ export const getFileURLFromVaultBySkyflowID = ( rootReject(err); } }); + +export const getFileURLFromVaultBySkyflowIDComposable = ( + skyflowIdRecord: IRevealRecord, + client: Client, + authToken: string, +): Promise => new Promise((rootResolve, rootReject) => { + try { + getFileURLForRender( + skyflowIdRecord, client, authToken as string, + ).then((resolvedResult: IRenderResponseType) => { + rootResolve(resolvedResult); + }).catch((err: any) => { + const errorData = formatForRenderFileFailure(err, skyflowIdRecord.skyflowID as string, + skyflowIdRecord.column as string); + printLog(errorData.error?.description || '', MessageType.ERROR, LogLevel.ERROR); + rootReject(errorData); + }); + } catch (err) { + rootReject(err); + } +}); export const fetchRecordsByTokenId = ( tokenIdRecords: IRevealRecord[], client: Client, @@ -235,6 +257,65 @@ export const fetchRecordsByTokenId = ( rootReject(err); }); }); + +export const fetchRecordsByTokenIdComposable = ( + tokenIdRecords: IRevealRecordComposable[], + client: Client, + authToken: string, +): Promise => new Promise((rootResolve, rootReject) => { + const vaultResponseSet: Promise[] = tokenIdRecords?.map( + (tokenRecord) => new Promise((resolve) => { + const apiResponse: any = []; + const redaction: RedactionType = tokenRecord?.redaction ?? RedactionType.PLAIN_TEXT; + + getTokenRecordsFromVault(tokenRecord?.token ?? '', redaction, client, authToken) + ?.then( + (response: IApiSuccessResponse) => { + const fieldsData = formatForPureJsSuccess(response); + apiResponse?.push({ + ...fieldsData, + frameId: tokenRecord?.iframeName ?? '', + }); + }, + (cause: any) => { + const errorData = formatForPureJsFailure(cause, tokenRecord?.token ?? ''); + printLog(errorData?.error?.description ?? '', MessageType.ERROR, LogLevel.ERROR); + apiResponse?.push({ + ...errorData, + frameId: tokenRecord?.iframeName ?? '', + }); + }, + ) + ?.finally(() => { + resolve(apiResponse); + }); + }), + ); + + Promise.allSettled(vaultResponseSet)?.then((resultSet) => { + const recordsResponse: Record[] = []; + const errorResponse: Record[] = []; + resultSet?.forEach((result) => { + if (result?.status === 'fulfilled') { + result?.value?.forEach((res: Record) => { + if (Object.prototype.hasOwnProperty.call(res, 'error')) { + errorResponse?.push(res); + } else { + recordsResponse?.push(res); + } + }); + } + }); + if (errorResponse?.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse?.length === 0) { + rootReject({ errors: errorResponse }); + } else { + rootReject({ records: recordsResponse, errors: errorResponse }); + } + }); +}); + export const formatRecordsForIframe = (response: IRevealResponseType) => { const result: Record = {}; if (response.records) { @@ -264,6 +345,7 @@ export const formatForRenderClient = (response: IRenderResponseType, column: str const successRecord = { skyflow_id: response.fields.skyflow_id, column, + fileMetadata: response.fileMetadata, }; formattedResponse.success = successRecord; } else if (response.errors) { @@ -295,6 +377,34 @@ export const formatRecordsForClient = (response: IRevealResponseType): RevealRes return revealResponse; }; +export const formatRecordsForClientComposable = (response) => { + let successRecords = []; + let errorRecords = []; + + if (response?.errors && response?.errors?.length > 0) { + errorRecords = response?.errors?.map((errors) => ({ + error: errors?.error ?? {}, + })); + } + + if (response?.records) { + successRecords = response?.records?.map((record) => ({ + token: record?.[0]?.token ?? '', + valueType: record?.[0]?.valueType ?? '', + })); + } + + if (successRecords?.length > 0 && errorRecords?.length > 0) { + return { success: successRecords, errors: errorRecords }; + } + + if (successRecords?.length > 0) { + return { success: successRecords }; + } + + return { errors: errorRecords }; +}; + export const fetchRecordsGET = async ( skyflowIdRecords: IGetRecord[], client: Client, diff --git a/src/core/constants.ts b/src/core/constants.ts index d4e2d73d..8a674353 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -30,11 +30,13 @@ export const SDK_IFRAME_EVENT = 'SDK IFRAME EVENT'; export const DOMAIN = 'US2'; export const CORALOGIX_DOMAIN = 'https://cdn.rum-ingress-coralogix.com/coralogix/browser/latest/coralogix-browser-sdk.js'; export const FRAME_ELEMENT = 'element'; +export const COMPOSABLE_REVEAL = 'reveal-composable'; export const ELEMENT_TYPES = { COLLECT: 'COLLECT', REVEAL: 'REVEAL', COMPOSE: 'COMPOSABLE', + REVEAL_COMPOSE: 'REVEAL_COMPOSE', }; export const EVENT_TYPES = { @@ -105,8 +107,19 @@ export const ELEMENT_EVENTS_TO_CLIENT = { }; export const ELEMENT_EVENTS_TO_IFRAME = { + MULTIPLE_UPLOAD_FILES_RESPONSE: 'MULTIPLE_UPLOAD_FILES_RESPONSE', + RENDER_MOUNTED: 'RENDER_MOUNTED', + HEIGHT_CALLBACK: 'HEIGHT_CALLBACK', + HEIGHT_CALLBACK_COMPOSABLE: 'HEIGHT_CALLBACK_COMPOSABLE', + COMPOSABLE_REVEAL: 'COMPOSABLE_REVEAL', + MULTIPLE_UPLOAD_FILES: 'MULTIPLE_UPLOAD_FILES', COLLECT_CALL_REQUESTS: 'COLLECT_CALL_REQUESTS', + COMPOSABLE_CALL_REQUESTS: 'COMPOSABLE_CALL_REQUESTS', + COMPOSABLE_CALL_RESPONSE: 'COMPOSABLE_CALL_RESPONSE', + COMPOSABLE_FILE_CALL_RESPONSE: 'COMPOSABLE_FILE_CALL_RESPONSE', + COMPOSABLE_CONTAINER: 'COMPOSABLE_CONTAINER', REVEAL_CALL_REQUESTS: 'REVEAL_CALL_REQUESTS', + REVEAL_CALL_RESPONSE: 'REVEAL_CALL_RESPONSE', FRAME_READY: 'FRAME_READY', READY_FOR_CLIENT: 'READY_FOR_CLIENT', TOKENIZATION_REQUEST: 'TOKENIZATION_REQUEST', @@ -163,6 +176,7 @@ export enum ElementType { EXPIRATION_MONTH = 'EXPIRATION_MONTH', EXPIRATION_YEAR = 'EXPIRATION_YEAR', FILE_INPUT = 'FILE_INPUT', + MULTI_FILE_INPUT = 'MULTI_FILE_INPUT', } export enum CardType { @@ -325,6 +339,14 @@ export const ELEMENTS = { type: 'file', }, }, + [ElementType.MULTI_FILE_INPUT]: { + name: 'MULTI_FILE_INPUT', + sensitive: true, + attributes: { + type: 'file', + multiple: '', + }, + }, }; export const CARDNUMBER_INPUT_FORMAT = { @@ -638,6 +660,7 @@ export const DEFAULT_ERROR_TEXT_ELEMENT_TYPES = { [ElementType.EXPIRATION_MONTH]: 'Invalid expiration month', [ElementType.EXPIRATION_YEAR]: 'Invalid expiration year', [ElementType.FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, + [ElementType.MULTI_FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, }; export const DEFAULT_REQUIRED_TEXT_ELEMENT_TYPES = { @@ -650,6 +673,7 @@ export const DEFAULT_REQUIRED_TEXT_ELEMENT_TYPES = { [ElementType.EXPIRATION_MONTH]: 'expiration month is required', [ElementType.EXPIRATION_YEAR]: 'expiration year is required', [ElementType.FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, + [ElementType.MULTI_FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, }; export const INPUT_KEYBOARD_EVENTS = { diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 8dfc05de..7856b62c 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -42,6 +42,7 @@ import { updateMetricObjectValue, } from '../../../metrics'; import { Metadata, ContainerProps, InternalState } from '../../internal/internal-types'; +import properties from '../../../properties'; const CLASS_NAME = 'Element'; class CollectElement extends SkyflowElement { @@ -120,7 +121,6 @@ class CollectElement extends SkyflowElement { // if (this.#isSingleElementAPI && this.#elements.length > 1) { // throw new SkyflowError(SKYFLOW_ERROR_CODE.UNKNOWN_ERROR, [], true); // } - this.#doesReturnValue = EnvOptions[this.#context.env].doesReturnValue; this.elementType = this.#isSingleElementAPI ? this.#elements[0].elementType @@ -140,6 +140,7 @@ class CollectElement extends SkyflowElement { name: element.elementName, isRequired: element.required, selectedCardScheme: '', + metaData: undefined, }); }); if (this.#elements && this.#elements.length && this.#elements.length > 1) { @@ -165,18 +166,18 @@ class CollectElement extends SkyflowElement { this.#readyToMount = container.isMounted; if (container.type === ContainerType.COMPOSABLE) { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + + this.#iframe.name) { + this.#iframe.setIframeHeight(event.data.data.height); + } + }); this.#elements.forEach((element) => { - this.#bus.on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED - + formatFrameNameToId(element.elementName), (data) => { - if (data.name === element.elementName) { - updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, `${element.elementType}_${METRIC_TYPES.EVENTS.MOUNTED}`); + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + formatFrameNameToId(element.elementName)) { element.isMounted = true; this.#mounted = true; - this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT - + this.#iframe.name, - {}, (payload:any) => { - this.#iframe.setIframeHeight(payload.height); - }); } }); }); @@ -200,15 +201,41 @@ class CollectElement extends SkyflowElement { getID = () => this.#elementId; mount = (domElement: HTMLElement | string) => { + this.#mounted = true; if (!domElement) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['CollectElement'], true); } - this.resizeObserver = new ResizeObserver(() => { - this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - {}, (payload:any) => { - this.#iframe.setIframeHeight(payload.height); - }); - }); + + if (domElement instanceof HTMLElement) { + this.resizeObserver = new ResizeObserver(() => { + const iframeElements = domElement.getElementsByTagName('iframe'); + if (iframeElements && iframeElements.length > 0) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name + && iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN, + ); + } + } + } + }); + } else if (typeof domElement === 'string') { + this.resizeObserver = new ResizeObserver(() => { + this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + }); + } + updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElement); if ( this.#metaData?.clientJSON?.config?.options?.trackMetrics @@ -469,84 +496,168 @@ class CollectElement extends SkyflowElement { } }); this.#elements.forEach((element1) => { - this.#bus.on(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT - + element1.elementName, (data: any) => { - if ( - this.#isSingleElementAPI + const isComposableContainer = this.#metaData?.containerType === 'COMPOSABLE'; + if (isComposableContainer) { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + + element1.elementName) { + const data = event.data.data; + if (data.name === element1.elementName) { + if ( + this.#isSingleElementAPI && data.event === ELEMENT_EVENTS_TO_CLIENT.READY && data.name === formatFrameNameToId(this.#iframe.name) - ) { - this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); - } else { - const isComposable = this.#elements.length > 1; - this.#elements.forEach((element, index) => { - if (data.name === element.elementName) { - let emitEvent = ''; - switch (data.event) { - case ELEMENT_EVENTS_TO_CLIENT.FOCUS: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.FOCUS; - break; - case ELEMENT_EVENTS_TO_CLIENT.BLUR: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.BLUR; - break; - case ELEMENT_EVENTS_TO_CLIENT.CHANGE: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.CHANGE; - break; - case ELEMENT_EVENTS_TO_CLIENT.READY: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.READY; - break; - case ELEMENT_EVENTS_TO_CLIENT.SUBMIT: - this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.SUBMIT); - return; - // case ELEMENT_EVENTS_TO_CLIENT.CREATED: - // this.#mounted = true; - // return; - // todo: need to implement the below events - // case ELEMENT_EVENTS_TO_CLIENT.ESCAPE: - // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ESCAPE); - // break; - // case ELEMENT_EVENTS_TO_CLIENT.CLICK: - // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.CLICK); - // break; - // case ELEMENT_EVENTS_TO_CLIENT.ERROR: - // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ERROR); - // break; - - default: - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_EVENT_TYPE, [], true); - } - this.#states[index].isEmpty = data.value.isEmpty; - this.#states[index].isValid = data.value.isValid; - this.#states[index].isComplete = data.value.isComplete; - this.#states[index].isFocused = data.value.isFocused; - this.#states[index].isRequired = data.value.isRequired; - this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; - - if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; - else this.#states[index].value = undefined; - - emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; - - this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT - + this.#iframe.name, - {}, (payload:any) => { - this.#iframe.setIframeHeight(payload.height); - }); - - this.#updateState(); - const emitData = { - ...this.#states[index], - elementType: element.elementType, - }; - if (isComposable && this.#groupEmitter) { - this.#groupEmitter._emit(emitEvent, emitData); + ) { + this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); } else { - this.#eventEmitter._emit(emitEvent, emitData); + this.#elements.forEach((element, index) => { + if (data.name === element.elementName) { + let emitEvent = ''; + switch (data.event) { + case ELEMENT_EVENTS_TO_CLIENT.FOCUS: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.FOCUS; + break; + case ELEMENT_EVENTS_TO_CLIENT.BLUR: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.BLUR; + break; + case ELEMENT_EVENTS_TO_CLIENT.CHANGE: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.CHANGE; + break; + case ELEMENT_EVENTS_TO_CLIENT.READY: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.READY; + break; + case ELEMENT_EVENTS_TO_CLIENT.SUBMIT: + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.SUBMIT); + return; + + default: + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_EVENT_TYPE, [], true); + } + this.#states[index].isEmpty = data.value.isEmpty; + this.#states[index].isValid = data.value.isValid; + this.#states[index].isComplete = data.value.isComplete; + this.#states[index].isFocused = data.value.isFocused; + this.#states[index].isRequired = data.value.isRequired; + this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; + if (element.elementType === ElementType.MULTI_FILE_INPUT + || element.elementType === ElementType.FILE_INPUT) { + this.#states[index].metaData = data?.value?.metaData || []; + } + if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; + else this.#states[index].value = undefined; + + emitEvent = isComposableContainer ? `${emitEvent}:${data.name}` : emitEvent; + this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + + this.#updateState(); + const emitData = { + ...this.#states[index], + elementType: element.elementType, + }; + if (isComposableContainer) { + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, { + iframeName: this.#iframe.name, + }); + } + if (isComposableContainer && this.#groupEmitter) { + this.#groupEmitter._emit(emitEvent, emitData); + } else { + this.#eventEmitter._emit(emitEvent, emitData); + } + } + }); } } - }); - } - }); + } + }); + } else { + this.#bus.on(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + + element1.elementName, (data: any) => { + if ( + this.#isSingleElementAPI + && data.event === ELEMENT_EVENTS_TO_CLIENT.READY + && data.name === formatFrameNameToId(this.#iframe.name) + ) { + this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); + } else { + const isComposable = this.#elements.length > 1; + this.#elements.forEach((element, index) => { + if (data.name === element.elementName) { + let emitEvent = ''; + switch (data.event) { + case ELEMENT_EVENTS_TO_CLIENT.FOCUS: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.FOCUS; + break; + case ELEMENT_EVENTS_TO_CLIENT.BLUR: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.BLUR; + break; + case ELEMENT_EVENTS_TO_CLIENT.CHANGE: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.CHANGE; + break; + case ELEMENT_EVENTS_TO_CLIENT.READY: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.READY; + break; + case ELEMENT_EVENTS_TO_CLIENT.SUBMIT: + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.SUBMIT); + return; + // case ELEMENT_EVENTS_TO_CLIENT.CREATED: + // this.#mounted = true; + // return; + // todo: need to implement the below events + // case ELEMENT_EVENTS_TO_CLIENT.ESCAPE: + // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ESCAPE); + // break; + // case ELEMENT_EVENTS_TO_CLIENT.CLICK: + // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.CLICK); + // break; + // case ELEMENT_EVENTS_TO_CLIENT.ERROR: + // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ERROR); + // break; + + default: + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_EVENT_TYPE, [], true); + } + this.#states[index].isEmpty = data.value.isEmpty; + this.#states[index].isValid = data.value.isValid; + this.#states[index].isComplete = data.value.isComplete; + this.#states[index].isFocused = data.value.isFocused; + this.#states[index].isRequired = data.value.isRequired; + this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; + + if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; + else this.#states[index].value = undefined; + + emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; + this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + + this.#updateState(); + const emitData = { + ...this.#states[index], + elementType: element.elementType, + }; + if (isComposable) { + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, { + iframeName: this.#iframe.name, + }); + } + if (isComposable && this.#groupEmitter) { + this.#groupEmitter._emit(emitEvent, emitData); + } else { + this.#eventEmitter._emit(emitEvent, emitData); + } + } + }); + } + }); + } }); }; diff --git a/src/core/external/collect/compose-collect-container.ts b/src/core/external/collect/compose-collect-container.ts index c3f7d0ef..d8915d0f 100644 --- a/src/core/external/collect/compose-collect-container.ts +++ b/src/core/external/collect/compose-collect-container.ts @@ -24,6 +24,7 @@ import { InputStyles, ErrorTextStyles, ContainerOptions, + UploadFilesResponse, } from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import logs from '../../../utils/logs'; @@ -43,6 +44,8 @@ import CollectElement from './collect-element'; import ComposableElement from './compose-collect-element'; import { ElementGroup, ElementGroupItem } from './collect-container'; import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; +import Client from '../../../client'; +import { getAccessToken } from '../../../utils/bus-events'; export interface ComposableElementGroup extends ElementGroup { styles: InputStyles; @@ -81,7 +84,13 @@ class ComposableContainer extends Container { #clientDomain: string = ''; - #isSkyflowFrameReady: boolean = false; + #isComposableFrameReady: boolean = false; + + #shadowRoot: ShadowRoot | null = null; + + #iframeID: string = ''; + + #getSkyflowBearerToken: () => Promise | undefined; constructor( metaData: Metadata, @@ -104,8 +113,7 @@ class ComposableContainer extends Container { }, }, }; - this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; - + this.#getSkyflowBearerToken = metaData.getSkyflowBearerToken; this.#skyflowElements = skyflowElements; this.#context = context; this.#options = options; @@ -125,6 +133,18 @@ class ComposableContainer extends Container { this.#context.logLevel); this.#containerMounted = true; this.#updateListeners(); + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.#containerId, (data, callback) => { + printLog(parameterizedString(logs.infoLogs.INITIALIZE_COMPOSABLE_CLIENT, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + callback({ + client: this.#metaData.clientJSON, + context, + }); + this.#isComposableFrameReady = true; + }); } create = (input: CollectElementInput, options: CollectElementOptions = { @@ -150,7 +170,11 @@ class ComposableContainer extends Container { elementName, }); const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`; - return new ComposableElement(elementName, this.#eventEmitter, controllerIframeName); + this.#iframeID = controllerIframeName; + return new ComposableElement( + elementName, this.#eventEmitter, controllerIframeName, + { ...this.#metaData, type: input.type }, + ); }; #createMultipleElement = ( @@ -316,156 +340,228 @@ class ComposableContainer extends Container { this.#containerElement.mount(domElement); this.#isMounted = true; } + this.#elementsList.forEach((element) => { + this.#eventEmitter.on(`${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, (data, callback) => { + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, + { + elementName: element.name, + data: { + type: COLLECT_TYPES.FILE_UPLOAD, + containerId: this.#containerId, + }, + clientConfig: { + vaultURL: this.#metaData?.clientJSON?.config?.vaultURL, + vaultID: this.#metaData?.clientJSON?.config?.vaultID, + authToken, + }, + options: { + ...data?.options, + }, + }, + ); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + callback(err); + }); + }); + }); + if (domElement instanceof HTMLElement + && (domElement as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = domElement.getRootNode() as ShadowRoot; + } else if (typeof domElement === 'string') { + const element = document.getElementById(domElement); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = element.getRootNode() as ShadowRoot; + } + } + if (this.#shadowRoot !== null) { + this.#eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, (data) => { + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + data.iframeName, {}); + }); + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframeID, {}); + } }; unmount = () => { this.#containerElement.unmount(); }; - collect = (options: ICollectOptions = { tokens: true }): Promise => { - this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; - if (this.#isSkyflowFrameReady) { - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - if (!this.#isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); - } - const containerElements = getElements(this.#tempElements); - containerElements.forEach((element:any) => { - if (!element?.isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } - }); - const elementIds:{ frameId:string, elementId:string }[] = []; - const collectElements = Object.values(this.#elements); - collectElements.forEach((element) => { - element.isValidElement(); - }); - if (options && options.tokens && typeof options.tokens !== 'boolean') { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); - } - if (options?.additionalFields) { - validateAdditionalFieldsInCollect(options.additionalFields); - } - if (options?.upsert) { - validateUpsertOptions(options?.upsert); + collect = (options: ICollectOptions = { tokens: true }) : + Promise => new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + if (!this.#isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); + } + const containerElements = getElements(this.#tempElements); + containerElements.forEach((element:any) => { + if (!element?.isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); + } + }); + const elementIds:{ frameId:string, elementId:string }[] = []; + const collectElements = Object.values(this.#elements); + collectElements.forEach((element) => { + element.isValidElement(); + }); + if (options && options.tokens && typeof options.tokens !== 'boolean') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); + } + if (options?.additionalFields) { + validateAdditionalFieldsInCollect(options.additionalFields); + } + if (options?.upsert) { + validateUpsertOptions(options?.upsert); + } + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: this.#tempElements.elementName, + elementId: element.elementName ?? '', + }); + }); + const client = Client.fromJSON(this.#metaData.clientJSON) as any; + const clientId = client.toJSON()?.metaData?.uuid || ''; + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.#containerId, { + data: { + type: COLLECT_TYPES.COLLECT, + ...options, + tokens: options?.tokens !== undefined ? options.tokens : true, + elementIds, + containerId: this.#containerId, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + window.addEventListener('message', (event) => { + if (event.data?.type + === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.#containerId) { + const data = event.data.data; + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); + reject(data?.error); + } else if (data?.records) { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + resolve(data); + } else { + printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.#context.logLevel); + reject(data); } - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: this.#tempElements.elementName, - elementId: element.elementName || '', - }); - }); - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.COLLECT, - ...options, - tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - resolve(data); - } - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), - MessageType.LOG, this.#context.logLevel); - } catch (err:any) { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); } }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), + MessageType.LOG, this.#context.logLevel); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - if (!this.#isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); - } + }); + + #emitEvent = (eventName: string, options?: Record, callback?: any) => { + if (this.#shadowRoot) { + const iframe = this.#shadowRoot.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } else { + const iframe = document.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }; - const containerElements = getElements(this.#tempElements); - containerElements.forEach((element:any) => { - if (!element?.isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } + uploadFiles = (options: ICollectOptions): + Promise => new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + if (!this.#isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); + } + const elementIds:{ frameId:string, elementId:string }[] = []; + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: this.#tempElements.elementName, + elementId: element.elementName ?? '', }); - const elementIds:{ frameId:string, elementId:string }[] = []; - const collectElements = Object.values(this.#elements); - collectElements.forEach((element) => { - element.isValidElement(); + }); + const client = Client.fromJSON(this.#metaData.clientJSON) as any; + const clientId = client.toJSON()?.metaData?.uuid || ''; + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.#containerId, { + data: { + type: COLLECT_TYPES.FILE_UPLOAD, + ...options, + // tokens: options?.tokens !== undefined ? options.tokens : true, + elementIds, + containerId: this.#containerId, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, }); - - if (options && options.tokens && typeof options.tokens !== 'boolean') { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); - } - if (options?.additionalFields) { - validateAdditionalFieldsInCollect(options.additionalFields); - } - if (options?.upsert) { - validateUpsertOptions(options?.upsert); - } - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: this.#tempElements.elementName, - elementId: element.elementName || '', - }); + window.addEventListener('message', (event) => { + if (event.data?.type + === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.#containerId) { + const data = event.data.data; + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); + reject(data?.error); + } else if (data?.fileUploadResponse) { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + resolve(data); + } else { + printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.#context.logLevel); + reject(data); + } + } }); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.#containerId, () => { - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.COLLECT, - ...options, - tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - resolve(data); - } - }, - ); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), - MessageType.LOG, this.#context.logLevel); - } catch (err:any) { + }).catch((err:any) => { printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); reject(err); - } - }); - }; + }); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + } + }); #updateListeners = () => { this.#eventEmitter.on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_UPDATE_OPTIONS, (data) => { diff --git a/src/core/external/collect/compose-collect-element.ts b/src/core/external/collect/compose-collect-element.ts index b3e7b7c3..b5aa0837 100644 --- a/src/core/external/collect/compose-collect-element.ts +++ b/src/core/external/collect/compose-collect-element.ts @@ -1,10 +1,15 @@ +import { Context } from 'vm'; import EventEmitter from '../../../event-emitter'; import { formatValidations } from '../../../libs/element-options'; import SkyflowError from '../../../libs/skyflow-error'; import { ContainerType } from '../../../skyflow'; -import { CollectElementUpdateOptions, EventName } from '../../../utils/common'; +import { + CollectElementUpdateOptions, EventName, MessageType, MetaData, +} from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ElementType } from '../../constants'; +import { printLog } from '../../../utils/logs-helper'; +import logs from '../../../utils/logs'; class ComposableElement { #elementName: string; @@ -19,7 +24,13 @@ class ComposableElement { #isUpdateCalled = false; - constructor(name: string, eventEmitter: EventEmitter, iframeName: string) { + #metaData: any; + + #context: Context; + + #elementType: ElementType; + + constructor(name, eventEmitter, iframeName, metaData) { this.#elementName = name; this.#iframeName = iframeName; this.#eventEmitter = eventEmitter; @@ -27,6 +38,12 @@ class ComposableElement { this.#eventEmitter.on(`${EventName.READY}:${this.#elementName}`, () => { this.#isMounted = true; }); + this.#metaData = metaData; + this.#context = { + logLevel: this.#metaData?.clientJSON?.config?.options?.logLevel, + env: this.#metaData?.clientJSON?.config?.options?.env, + }; + this.#elementType = this.#metaData?.type as ElementType; } on(eventName: string, handler: Function) { @@ -98,6 +115,40 @@ class ComposableElement { }); } }; -} + uploadMultipleFiles = (metaData?: MetaData) => new Promise((resolve, reject) => { + try { + if (this.#elementType !== ElementType.MULTI_FILE_INPUT) { + throw new SkyflowError( + SKYFLOW_ERROR_CODE.MULTI_FILE_NOT_SUPPORTED, + [], + true, + ); + } + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter._emit(`${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${this.#elementName}`, { + options: metaData, + }, (response: any) => { + if (response.error) { + reject(response); + } + }); + window.addEventListener('message', (event) => { + if (event?.data?.type === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${this.#elementName}`) { + if (event?.data?.data?.errorResponse || event?.data?.data?.error) { + printLog(`${event?.data?.data.errorResponse || event?.data?.data.error}`, MessageType.ERROR, this.#context.logLevel); + reject(event?.data?.data); + } else { + printLog(logs.infoLogs.MULTI_UPLOAD_FILES_SUCCESS, + MessageType.LOG, this.#context.logLevel); + resolve(event?.data?.data); + } + } + }); + } catch (error) { + printLog(`${error}`, MessageType.ERROR, this.#context.logLevel); + reject(error); + } + }); +} export default ComposableElement; diff --git a/src/core/external/common/iframe.ts b/src/core/external/common/iframe.ts index defcf630..96304fbf 100644 --- a/src/core/external/common/iframe.ts +++ b/src/core/external/common/iframe.ts @@ -28,11 +28,12 @@ export default class IFrame { this.iframe = iframer({ name: this.name, referrer: clientDomain, + title: name.match(/^element:([^:]+):/)?.[1] ?? name, }); } - mount = (domElement: HTMLElement | string, elementId?: string, data?: any) => { - this.unmount(); + mount = (domElement, elementId?: string, data?: any) => { + // this.unmount(); try { if (typeof domElement === 'string') { this.container = document.querySelector(domElement) || undefined; diff --git a/src/core/external/reveal/composable-reveal-container.ts b/src/core/external/reveal/composable-reveal-container.ts new file mode 100644 index 00000000..4eb2cb14 --- /dev/null +++ b/src/core/external/reveal/composable-reveal-container.ts @@ -0,0 +1,480 @@ +/* eslint-disable no-plusplus */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +import bus from 'framebus'; +import sum from 'lodash/sum'; +import EventEmitter from '../../../event-emitter'; +import iframer, { setAttributes, getIframeSrc, setStyles } from '../../../iframe-libs/iframer'; +import deepClone from '../../../libs/deep-clone'; +import SkyflowError from '../../../libs/skyflow-error'; +import uuid from '../../../libs/uuid'; +import properties from '../../../properties'; +import { ContainerType } from '../../../skyflow'; +import { + Context, MessageType, +} from '../../../utils/common'; +import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import logs from '../../../utils/logs'; +import { printLog, parameterizedString } from '../../../utils/logs-helper'; +import { + validateInitConfig, + validateInputFormatOptions, + validateRevealElementRecords, +} from '../../../utils/validators'; +import { + COLLECT_FRAME_CONTROLLER, + CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, + FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, + COMPOSABLE_REVEAL, + REVEAL_TYPES, +} from '../../constants'; +import Container from '../common/container'; + +import ComposableRevealElement from './composable-reveal-element'; +import { ContainerOptions, RevealElementInput, RevealResponse } from '../../../index-node'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; +import ComposableRevealInternalElement from './composable-reveal-internal'; +import { formatRevealElementOptions } from '../../../utils/helpers'; +import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; +import ComposableContainer, { ComposableElementGroup } from '../collect/compose-collect-container'; + +const CLASS_NAME = 'ComposableRevealContainer'; +class ComposableRevealContainer extends Container { + #containerId: string; + + #elements: Record = {}; + + #metaData: Metadata; + + #elementGroup: any = { rows: [] }; + + #elementsList:any = []; + + #context: Context; + + #skyflowElements: Array; + + #eventEmitter: EventEmitter; + + #isMounted: boolean = false; + + #options: any; + + #containerElement:any; + + type:string = ContainerType.COMPOSE_REVEAL; + + #containerMounted: boolean = false; + + #tempElements: any = {}; + + #clientDomain: string = ''; + + #isComposableFrameReady: boolean = false; + + #shadowRoot: ShadowRoot | null = null; + + #iframeID: string = ''; + + #revealRecords: IRevealElementInput[] = []; + + #getSkyflowBearerToken: () => Promise | undefined; + + constructor( + metaData: Metadata, + skyflowElements:Array, + context: Context, + options?: ContainerOptions, + ) { + super(); + this.#containerId = uuid(); + this.#metaData = { + ...metaData, + clientJSON: { + ...metaData?.clientJSON, + config: { + ...metaData?.clientJSON?.config, + options: { + ...metaData?.clientJSON?.config?.options, + ...options, + }, + }, + }, + }; + this.#getSkyflowBearerToken = metaData?.getSkyflowBearerToken; + this.#skyflowElements = skyflowElements; + this.#context = context; + this.#options = options; + this.#eventEmitter = new EventEmitter(); + + this.#clientDomain = this.#metaData.clientDomain || ''; + const iframe = iframer({ + name: `${COLLECT_FRAME_CONTROLLER}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`, + referrer: this.#clientDomain, + }); + setAttributes(iframe, { + src: getIframeSrc(), + }); + setStyles(iframe, { ...CONTROLLER_STYLES }); + printLog(parameterizedString(logs.infoLogs.CREATE_COLLECT_CONTAINER, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#containerMounted = true; + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.#containerId, (data, callback) => { + printLog(parameterizedString(logs.infoLogs.INITIALIZE_COMPOSABLE_CLIENT, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + callback({ + client: this.#metaData.clientJSON, + context, + }); + this.#isComposableFrameReady = true; + }); + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + this.#containerId) { + this.#isComposableFrameReady = true; + } + }); + } + + create = (input: RevealElementInput, options?: IRevealElementOptions) => { + const elementId = uuid(); + validateInputFormatOptions(options); + + const elementName = `${COMPOSABLE_REVEAL}:${btoa(elementId)}`; + this.#elementsList?.push({ + name: elementName, + ...input, + elementName, + elementId, + ...formatRevealElementOptions(options ?? {}), + }); + const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements ?? {})}:${this.#containerId}:${this.#context?.logLevel}:${btoa(this.#clientDomain ?? '')}`; + return new ComposableRevealElement(elementName, + this.#eventEmitter, + controllerIframeName); + }; + + #createMultipleElement = ( + multipleElements: ComposableElementGroup, + isSingleElementAPI: boolean = false, + ): ComposableContainer => { + try { + const elements: any[] = []; + this.#tempElements = deepClone(multipleElements); + this.#tempElements?.rows?.forEach((row) => { + row?.elements?.forEach((element) => { + const options = element ?? {}; + const { elementType } = options; + options.isMounted = false; + + options.label = element?.label; + options.skyflowID = element?.skyflowID; + + elements.push(options); + }); + }); + + this.#tempElements.elementName = isSingleElementAPI + ? elements[0].elementName + : `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}`; + if ( + isSingleElementAPI + && !this.#elements[elements[0].elementName] + && this.#hasElementName(elements[0].name) + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); + } + + let element = this.#elements[this.#tempElements.elementName]; + if (element) { + if (isSingleElementAPI) { + // element.update(elements[0]); + } else { + // element.update(this.#tempElements); + } + } else { + const elementId = uuid(); + try { + element = new ComposableRevealInternalElement( + elementId, + this.#tempElements, + this.#metaData, + { + containerId: this.#containerId, + isMounted: this.#containerMounted, + type: this.type, + eventEmitter: this.#eventEmitter, + }, + this.#context, + ); + this.#elements[this.#tempElements.elementName] = element; + this.#skyflowElements[elementId] = element; + } catch (error: any) { + printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, + MessageType.ERROR, + this.#context.logLevel); + throw error; + } + } + this.#iframeID = element.iframeName(); + return element; + } catch (error: any) { + printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, + MessageType.ERROR, + this.#context.logLevel); + throw error; + } + }; + + #hasElementName = (name: string) => { + const tempElements = Object.keys(this.#elements); + for (let i = 0; i < tempElements.length; i += 1) { + if (atob(tempElements[i].split(':')[2]) === name) { + return true; + } + } + return false; + }; + + mount = (domElement: HTMLElement | string) => { + if (!domElement) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, + ['RevealElement'], true); + } + + const { layout } = this.#options; + if (sum(layout) !== this.#elementsList.length) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISMATCH_ELEMENT_COUNT_LAYOUT_SUM, [], true); + } + let count = 0; + layout.forEach((rowCount, index) => { + this.#elementGroup.rows = [ + ...this.#elementGroup.rows, + { elements: [] }, + ]; + for (let i = 0; i < rowCount; i++) { + this.#elementGroup.rows[index].elements.push( + this.#elementsList[count], + ); + count++; + } + }); + if (this.#options.styles) { + this.#elementGroup.styles = { + ...this.#options.styles, + }; + } + if (this.#options.errorTextStyles) { + this.#elementGroup.errorTextStyles = { + ...this.#options.errorTextStyles, + }; + } + if (this.#containerMounted) { + this.#containerElement = this.#createMultipleElement(this.#elementGroup, false); + this.#containerElement.mount(domElement); + this.#isMounted = true; + } + if (domElement instanceof HTMLElement + && (domElement as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = domElement.getRootNode() as ShadowRoot; + } else if (typeof domElement === 'string') { + const element = document.getElementById(domElement); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = element.getRootNode() as ShadowRoot; + } + } + if (this.#shadowRoot !== null) { + this.#eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, (data) => { + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + data.iframeName, {}); + }); + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframeID, {}); + } + }; + + unmount = () => { + this.#containerElement.unmount(); + }; + + #emitEvent = (eventName: string, options?: Record, callback?: any) => { + if (this.#shadowRoot) { + const iframe = this.#shadowRoot.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } else { + const iframe = document.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }; + + reveal(): Promise { + this.#revealRecords = []; + if (this.#isComposableFrameReady) { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#elementsList.forEach((currentElement) => { + // if (currentElement.isClientSetError()) { + // throw new SkyflowError(SKYFLOW_ERROR_CODE.REVEAL_ELEMENT_ERROR_STATE); + // } + if (!currentElement.skyflowID) { + this.#revealRecords.push(currentElement); + } + }); + validateRevealElementRecords(this.#revealRecords); + const elementIds:{ frameId:string, token:string }[] = []; + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: element.name, + token: element.token, + }); + }); + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.#containerId, + { + data: { + type: REVEAL_TYPES.REVEAL, + containerId: this.#containerId, + elementIds, + }, + clientConfig: { + vaultURL: this.#metaData?.clientJSON?.config?.vaultURL, + vaultID: this.#metaData?.clientJSON?.config?.vaultID, + authToken, + }, + context: this.#context, + }, + ); + + window?.addEventListener('message', (event) => { + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId) { + const revealData = event?.data?.data; + if (revealData?.errors) { + printLog( + parameterizedString(logs?.errorLogs?.FAILED_REVEAL), + MessageType.ERROR, + this.#context?.logLevel, + ); + reject(revealData); + } else { + printLog( + parameterizedString(logs?.infoLogs?.REVEAL_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context?.logLevel, + ); + resolve(revealData); + } + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + } + }); + } + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#elementsList.forEach((currentElement) => { + // if (currentElement.isClientSetError()) { + // throw new SkyflowError(SKYFLOW_ERROR_CODE.REVEAL_ELEMENT_ERROR_STATE); + // } + if (!currentElement.skyflowID) { + this.#revealRecords.push(currentElement); + } + }); + validateRevealElementRecords(this.#revealRecords); + const elementIds:{ frameId:string, token:string }[] = []; + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: element.name, + token: element.token, + }); + }); + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + window.addEventListener('message', (messagEevent) => { + if (messagEevent?.data?.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + this.#containerId) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.#containerId, { + data: { + type: REVEAL_TYPES.REVEAL, + containerId: this.#containerId, + elementIds, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + context: this.#context, + }, + ); + window.addEventListener('message', (event) => { + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId) { + const revealData = event?.data?.data; + if (revealData?.errors) { + printLog(parameterizedString(logs.errorLogs.FAILED_REVEAL), + MessageType.ERROR, this.#context.logLevel); + reject(revealData); + } else { + printLog(parameterizedString(logs.infoLogs.REVEAL_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + resolve(revealData); + } + } + }); + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + } + }); + } +} +export default ComposableRevealContainer; diff --git a/src/core/external/reveal/composable-reveal-element.ts b/src/core/external/reveal/composable-reveal-element.ts new file mode 100644 index 00000000..539a017d --- /dev/null +++ b/src/core/external/reveal/composable-reveal-element.ts @@ -0,0 +1,66 @@ +import EventEmitter from '../../../event-emitter'; +import { ContainerType } from '../../../skyflow'; +import { EventName, RenderFileResponse } from '../../../utils/common'; +import { ELEMENT_EVENTS_TO_IFRAME, REVEAL_ELEMENT_OPTIONS_TYPES } from '../../constants'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; + +class ComposableRevealElement { + #elementName: string; + + #eventEmitter: EventEmitter; + + #iframeName: string; + + type: string = ContainerType?.COMPOSABLE ?? 'COMPOSABLE'; + + #isMounted: boolean = false; + + constructor(name: string, eventEmitter: EventEmitter, iframeName: string) { + this.#elementName = name; + this.#iframeName = iframeName; + this.#eventEmitter = eventEmitter; + this.#eventEmitter?.on?.(`${EventName?.READY ?? 'READY'}:${this.#elementName}`, () => { + this.#isMounted = true; + }); + } + + iframeName(): string { + return this.#iframeName ?? ''; + } + + getID(): string { + return this.#elementName ?? ''; + } + + renderFile(): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter?._emit?.( + `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST ?? ''}:${this.#elementName}`, + {}, + (response) => { + if (response?.errors) { + reject(response); + } else if (response?.error) { + reject({ errors: response?.error }); + } else { + resolve(response); + } + }, + ); + }); + } + + update = (options: IRevealElementInput | IRevealElementOptions) => { + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter?._emit?.( + `${ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS}:${this.#elementName}`, + { + options: options as IRevealElementInput | IRevealElementOptions, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + }, + ); + }; +} + +export default ComposableRevealElement; diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts new file mode 100644 index 00000000..bce2fe92 --- /dev/null +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -0,0 +1,541 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import bus from 'framebus'; +import SkyflowError from '../../../libs/skyflow-error'; +import uuid from '../../../libs/uuid'; +import { Context, MessageType, RenderFileResponse } from '../../../utils/common'; +import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import { + ELEMENT_EVENTS_TO_IFRAME, + ELEMENT_EVENTS_TO_CONTAINER, + REVEAL_ELEMENT_OPTIONS_TYPES, + METRIC_TYPES, + ELEMENT_EVENTS_TO_CLIENT, + EVENT_TYPES, + REVEAL_TYPES, + COMPOSABLE_REVEAL, +} from '../../constants'; +import IFrame from '../common/iframe'; +import SkyflowElement from '../common/skyflow-element'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; +import { + pushElementEventWithTimeout, + updateMetricObjectValue, +} from '../../../metrics'; +import logs from '../../../utils/logs'; +import { parameterizedString, printLog } from '../../../utils/logs-helper'; +import properties from '../../../properties'; +import { validateInitConfig, validateRenderElementRecord } from '../../../utils/validators'; +import EventEmitter from '../../../event-emitter'; +import { formatRevealElementOptions } from '../../../utils/helpers'; +import { Metadata, RevealContainerProps } from '../../internal/internal-types'; + +const CLASS_NAME = 'RevealElementInteranalElement'; + +export interface RevealComposableGroup{ + record: IRevealElementInput + options: IRevealElementOptions +} + +class ComposableRevealInternalElement extends SkyflowElement { + #iframe: IFrame; + + #metaData: Metadata; + + #recordData: any; + + #containerId: string; + + #isMounted:boolean = false; + + #isClientSetError:boolean = false; + + #context: Context; + + #elementId: string; + + resizeObserver: ResizeObserver | null; + + #readyToMount: boolean = false; + + #eventEmitter: EventEmitter; + + #shadowRoot: ShadowRoot | null = null; + + #getSkyflowBearerToken: () => Promise | undefined; + + #isComposableFrameReady: boolean = false; + + constructor(elementId: string, + recordGroup, + metaData: Metadata, + container: RevealContainerProps, + context: Context) { + super(); + this.#elementId = elementId; + this.#metaData = metaData; + this.resizeObserver = null; + this.#recordData = recordGroup; + this.#containerId = container?.containerId; + this.#readyToMount = container?.isMounted ?? true; + this.#eventEmitter = container?.eventEmitter; + this.#context = context; + + this.#iframe = new IFrame( + `${COMPOSABLE_REVEAL}:${btoa(uuid())}`, + metaData, + this.#containerId, + this.#context?.logLevel, + ); + + this.#readyToMount = true; + this.#getSkyflowBearerToken = metaData?.getSkyflowBearerToken; + + bus?.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe?.name, (data) => { + this.#iframe?.setIframeHeight(data?.height); + }); + + window?.addEventListener('message', (event) => { + if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + this.#iframe?.name) { + this.#iframe?.setIframeHeight(event?.data?.data?.height); + } + }); + + // eslint-disable-next-line max-len + if (this.#recordData?.rows) { + this.setupRenderFileEventListener(this.getRecordData()?.rows); + } + } + + private setupRenderFileEventListener(rows: any[]): void { + if (!rows?.length) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_REVEAL_COMPOSABLE_INPUT, ['COMPOSABLE_REVEAL'], true); + } + + try { + rows?.forEach((row, rowIndex) => { + row?.elements?.forEach((element: any, elementIndex: number) => { + if (!element?.name) return; + window?.addEventListener('message', (event) => { + if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + element?.name) { + this.#isComposableFrameReady = true; + } + }); + this.#eventEmitter?.on( + `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST}:${element?.name}`, + (data, callback) => { + this.renderFile(element)?.then((response) => { + callback?.(response); + })?.catch((error) => { + callback?.({ error }); + }); + }, + ); + this.#eventEmitter?.on( + `${ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS}:${element?.name}`, + (data) => { + if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS) { + // make this change in original elememt that is inside rows + const updatedElement = { + ...element, + ...data.options, + ...formatRevealElementOptions(data.options as IRevealElementOptions), + }; + + // Update element in this.#recordData.rows structure + if (this.#recordData?.rows?.[rowIndex]?.elements?.[elementIndex]) { + this.#recordData.rows[rowIndex].elements[elementIndex] = updatedElement; + } + + // Update local element reference + element = updatedElement; + + // Call update method + this.update(data.options, element); + } + }, + ); + }); + }); + } catch (error) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_REVEAL_COMPOSABLE_INPUT, ['COMPOSABLE_REVEAL'], true); + } + } + + getID() { + return this.#elementId; + } + + mount(domElementSelector: HTMLElement | string) { + if (!domElementSelector) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['RevealElement'], true); + } + + if (domElementSelector instanceof HTMLElement) { + this.resizeObserver = new ResizeObserver(() => { + const iframeElements = domElementSelector.getElementsByTagName('iframe'); + if (iframeElements && iframeElements.length > 0) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name + && iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN, + ); + } + } + } + }); + } + + updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElementSelector); + if ( + this.#metaData?.clientJSON?.config?.options?.trackMetrics + && this.#metaData.clientJSON.config?.options?.trackingKey + ) { + pushElementEventWithTimeout(this.#elementId); + } + + if (typeof domElementSelector === 'string') { + const targetElement = document.querySelector(domElementSelector); + if (targetElement) { + this.resizeObserver?.observe(targetElement); + } + } else if (domElementSelector instanceof HTMLElement) { + this.resizeObserver?.observe(domElementSelector); + } + + this.#readyToMount = true; + if (this.#readyToMount) { + this.#iframe.mount(domElementSelector, undefined, { + record: JSON.stringify({ + ...this.#metaData, + record: this.#recordData, + context: this.#context, + containerId: this.#containerId, + }), + }); + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + if (this.#recordData.skyflowID) { + bus + // .target(location.origin) + .emit( + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, + { + skyflowID: this.#recordData.skyflowID, + containerId: this.#containerId, + }, + ); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); + } else { + bus + // .target(location.origin) + .emit( + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, + { + id: this.#recordData.token, + containerId: this.#containerId, + }, + ); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); + } + if (Object.prototype.hasOwnProperty.call(this.#recordData, 'skyflowID')) { + bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + } + }); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.READY); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_START_TIME, Date.now()); + } + if (domElementSelector instanceof HTMLElement + && (domElementSelector as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = domElementSelector.getRootNode() as ShadowRoot; + } else if (typeof domElementSelector === 'string') { + const element = document.getElementById(domElementSelector); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = element.getRootNode() as ShadowRoot; + } + } + } + + #emitEvent = (eventName: string, options?: Record) => { + if (this.#shadowRoot) { + const iframe = this.#shadowRoot?.getElementById(this.#iframe?.name) as HTMLIFrameElement; + iframe?.contentWindow?.postMessage({ + name: eventName, + ...options, + }, properties?.IFRAME_SECURE_ORIGIN); + } else { + const iframe = document?.getElementById(this.#iframe?.name) as HTMLIFrameElement; + iframe?.contentWindow?.postMessage({ + name: eventName, + ...options, + }, properties?.IFRAME_SECURE_ORIGIN); + } + }; + + renderFile(recordData: any): Promise { + let altText = ''; + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + altText = recordData.altText; + } + this.setAltText('loading...', recordData); + const loglevel = this.#context.logLevel; + if (this.#isComposableFrameReady) { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), + MessageType.LOG, + loglevel); + validateRenderElementRecord(recordData); + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + recordData.name, + { + data: { + type: REVEAL_TYPES.RENDER_FILE, + containerId: this.#containerId, + iframeName: recordData.name, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + }, + ); + window?.addEventListener('message', (event) => { + if (event?.data && event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + + recordData.name) { + if (event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + const revealData = event?.data?.data?.result; + if (revealData?.error || revealData?.errors) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, + this.#context.logLevel); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText, recordData); + } + reject(revealData?.error || revealData?.errors); + } else { + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, recordData.skyflowID), + MessageType.LOG, this.#context.logLevel); + resolve(revealData); + } + } + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, loglevel); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, + loglevel); + reject(err); + } + }); + } + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), + MessageType.LOG, + loglevel); + validateRenderElementRecord(recordData); + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + recordData?.name) { + this.#isMounted = true; + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + recordData.name, + { + data: { + type: REVEAL_TYPES.RENDER_FILE, + containerId: this.#containerId, + iframeName: recordData.name, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + }, + ); + window.addEventListener('message', (event1) => { + if (event1?.data + && event1?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + + recordData.name) { + if (event1?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + const revealData = event1?.data?.data?.result; + if (revealData?.error || revealData?.errors) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, + this.#context.logLevel); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText, recordData); + } + reject(revealData?.error || revealData?.errors); + } else { + // eslint-disable-next-line max-len + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, recordData.skyflowID), + MessageType.LOG, this.#context.logLevel); + resolve(revealData); + } + } + } + }); + }).catch((err:any) => { + printLog(`${err?.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + } + }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, loglevel); + } catch (err: any) { + printLog(`Error: ${err?.message}`, MessageType.ERROR, + loglevel); + reject(err); + } + }); + } + + iframeName(): string { + return this.#iframe.name; + } + + isMounted():boolean { + return this.#isMounted; + } + + isClientSetError():boolean { + return this.#isClientSetError; + } + + getRecordData() { + return this.#recordData; + } + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + setErrorOverride(clientErrorText: string) { + // to be implemented + } + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + setError(clientErrorText:string) { + // to be implemented + } + + // eslint-disable-next-line class-methods-use-this + resetError() { + // to be implemented + } + + setAltText(altText:string, record) { + if (this.#isComposableFrameReady) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record?.name, + { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record?.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: altText, + }, + ); + } else { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + record?.name) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record?.name, + { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record?.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: altText, + }, + ); + } + }); + } + } + + // eslint-disable-next-line class-methods-use-this + clearAltText() { + // to be implemented + } + + unmount() { + if (this.#recordData.skyflowID) { + this.#isMounted = false; + this.#iframe.container?.remove(); + } + this.#isMounted = false; + this.#iframe.unmount(); + } + + update(options: IRevealElementInput | IRevealElementOptions, record) { + if (this.#isComposableFrameReady) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: options, + }, + ); + } else { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + record?.name) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: options, + }, + ); + } + }); + } + } +} + +export default ComposableRevealInternalElement; diff --git a/src/core/external/reveal/reveal-container.ts b/src/core/external/reveal/reveal-container.ts index fac8027a..f4e1b80b 100644 --- a/src/core/external/reveal/reveal-container.ts +++ b/src/core/external/reveal/reveal-container.ts @@ -79,7 +79,7 @@ class RevealContainer extends Container { options?: ContainerOptions, ) { super(); - this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; + this.#isSkyflowFrameReady = metaData?.skyflowContainer?.isControllerFrameReady; this.#metaData = { ...metaData, clientJSON: { @@ -150,6 +150,7 @@ class RevealContainer extends Container { containerId: this.#containerId, isMounted: this.#isMounted, eventEmitter: this.#eventEmmiter, + type: ContainerType.REVEAL, }, elementId, this.#context); this.#revealElements.push(revealElement); this.#skyflowElements[elementId] = revealElement; diff --git a/src/core/internal/composable-frame-element-init.ts b/src/core/internal/composable-frame-element-init.ts new file mode 100644 index 00000000..f78a091c --- /dev/null +++ b/src/core/internal/composable-frame-element-init.ts @@ -0,0 +1,369 @@ +import injectStylesheet from 'inject-stylesheet'; +import bus from 'framebus'; +import { getValueAndItsUnit } from '../../libs/element-options'; +import { getFlexGridStyles } from '../../libs/styles'; +import { ContainerType } from '../../skyflow'; +import { + Context, IRevealRecordComposable, +} from '../../utils/common'; +import { + getContainerType, +} from '../../utils/helpers'; +import { + ALLOWED_MULTIPLE_FIELDS_STYLES, + ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ERROR_TEXT_STYLES, REVEAL_TYPES, STYLE_TYPE, +} from '../constants'; +import IFrameFormElement from './iframe-form'; +import getCssClassesFromJss, { generateCssWithoutClass } from '../../libs/jss-styles'; +import FrameElement from '.'; +import Client from '../../client'; +import RevealFrame from './reveal/reveal-frame'; +import { + fetchRecordsByTokenIdComposable, formatRecordsForClientComposable, +} from '../../core-utils/reveal'; + +export default class RevealComposableFrameElementInit { + iframeFormElement: IFrameFormElement | undefined; + + clientMetaData: any; + + #domForm: HTMLFormElement; + + frameElement!: FrameElement; + + private static frameEle?: any; + + containerId: string; + + group: any; + + frameList: FrameElement[] = []; + + iframeFormList: IFrameFormElement[] = []; + + #client!: Client; + + #context!: Context; + + revealFrameList: any[] = []; + + rootDiv: HTMLDivElement; + + constructor() { + this.containerId = ''; + this.#domForm = document?.createElement('form'); + this.#domForm.action = '#'; + this.#domForm.onsubmit = (event) => { + event?.preventDefault(); + }; + + this.rootDiv = document?.createElement('div'); + this.updateGroupData(); + this.createContainerDiv(this.group); + + window?.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + + this.containerId && event?.data?.data?.type === REVEAL_TYPES.REVEAL) { + this.#context = event?.data?.context; + const data = event?.data?.data ?? {}; + const elementIds = data?.elementIds ?? []; + const revealDataInput: IRevealRecordComposable[] = []; + this.#client = new Client(event?.data?.clientConfig ?? {}, { + uuid: '', + clientDomain: '', + }); + + elementIds?.forEach((element) => { + this.revealFrameList?.forEach((revealFrame) => { + const data2 = revealFrame?.getData?.(); + if (data2?.name === element?.frameId) { + if (data2 && !data2?.skyflowID) { + const revealRecord: IRevealRecordComposable = { + token: data2?.token ?? '', + redaction: data2?.redaction, + iframeName: data2?.name ?? '', + }; + revealDataInput?.push(revealRecord); + } + } + }); + }); + + this.revealData(revealDataInput, this.containerId, event?.data?.clientConfig?.authToken) + ?.then((revealResponse: any) => { + if (revealResponse?.records?.length > 0) { + const formattedRecord = formatRecordsForClientComposable(revealResponse); + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.containerId, + data: formattedRecord, + }, + this.clientMetaData?.clientDomain, + ); + + revealResponse?.records?.forEach((record: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === record?.frameId) { + revealFrame?.responseUpdate?.(record); + } + }); + }); + } + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + }) + ?.catch((error) => { + const formattedRecord = formatRecordsForClientComposable(error); + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.containerId, + data: formattedRecord, + }, + this.clientMetaData?.clientDomain, + ); + + error?.records?.forEach((record: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === record?.frameId) { + revealFrame?.responseUpdate?.(record); + } + }); + }); + + error?.errors?.forEach((error1: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === error1?.frameId) { + revealFrame?.responseUpdate?.(error1); + } + }); + }); + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + }); + } + }); + + bus?.emit(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId, {}, (data: any) => { + this.#context = data?.context; + if (data?.client?.config) { + data.client.config = { + ...data?.client?.config, + }; + } + this.#client = Client?.fromJSON?.(data?.client); + }); + } + + updateGroupData = () => { + const url = window?.location?.href ?? ''; + const configIndex = url?.indexOf('?') ?? -1; + const encodedString = configIndex !== -1 ? decodeURIComponent(url?.substring(configIndex + 1)) : ''; + const parsedRecord = encodedString ? JSON.parse(atob(encodedString)) : {}; + this.clientMetaData = parsedRecord?.clientJSON?.metaData; + this.group = parsedRecord?.record; + this.containerId = parsedRecord?.containerId ?? ''; + this.#context = parsedRecord?.context; + }; + + static startFrameElement = () => { + RevealComposableFrameElementInit.frameEle = new RevealComposableFrameElementInit(); + }; + + revealData(revealRecords: IRevealRecordComposable[], containerId: string, authToken: string) { + return new Promise((resolve, reject) => { + fetchRecordsByTokenIdComposable(revealRecords, this.#client, authToken)?.then( + (resolvedResult) => { + resolve(resolvedResult); + }, + (rejectedResult) => { + reject(rejectedResult); + }, + ); + }); + } + + createContainerDiv = (newGroup) => { + this.group = newGroup; + const { + rows = [], + styles, + errorTextStyles, + } = this.group ?? {}; + const isComposableContainer = getContainerType(window?.name) === ContainerType?.COMPOSE_REVEAL; + this.group.spacing = getValueAndItsUnit(this.group?.spacing)?.join('') ?? ''; + this.rootDiv = document?.createElement('div'); + this.rootDiv.className = 'container'; + + const containerStylesByClassName = getFlexGridStyles({ + 'align-items': this.group?.alignItems ?? 'stretch', + 'justify-content': this.group?.justifyContent ?? 'flex-start', + spacing: this.group?.spacing, + }); + + injectStylesheet?.injectWithAllowlist( + { + [`.${this.rootDiv?.className}`]: containerStylesByClassName, + }, + ALLOWED_MULTIPLE_FIELDS_STYLES, + ); + + let count = 0; + rows?.forEach((row, rowIndex) => { + row.spacing = getValueAndItsUnit(row?.spacing)?.join('') ?? ''; + const rowDiv = document?.createElement('div'); + rowDiv.id = `row-${rowIndex}`; + const intialRowStyles = { + 'align-items': row?.alignItems ?? 'stretch', + 'justify-content': row?.justifyContent ?? 'flex-start', + spacing: row?.spacing, + padding: this.group?.spacing, + }; + + const rowStylesByClassName = getFlexGridStyles(intialRowStyles); + let errorTextElement; + + if (isComposableContainer) { + rowDiv.className = `${rowDiv?.id} SkyflowElement-${rowDiv?.id}-base`; + const rowStyles = { + [STYLE_TYPE?.BASE]: { + ...(styles?.[STYLE_TYPE?.BASE] ?? {}), + }, + }; + + getCssClassesFromJss?.(rowStyles, `${rowDiv?.id}`); + + errorTextElement = document?.createElement('span'); + errorTextElement.id = `${rowDiv?.id}-error`; + errorTextElement.className = 'SkyflowElement-row-error-base'; + + const errorStyles = { + [STYLE_TYPE?.BASE]: { + ...ERROR_TEXT_STYLES, + ...(errorTextStyles?.[STYLE_TYPE?.BASE] ?? {}), + }, + }; + getCssClassesFromJss?.(errorStyles, 'row-error'); + if (errorTextStyles?.[STYLE_TYPE?.GLOBAL]) { + generateCssWithoutClass?.(errorTextStyles?.[STYLE_TYPE?.GLOBAL]); + } + } else { + rowDiv.className = `row-${rowIndex}`; + injectStylesheet?.injectWithAllowlist( + { + [`.${rowDiv?.className}`]: rowStylesByClassName, + }, + ALLOWED_MULTIPLE_FIELDS_STYLES, + ); + } + + row?.elements?.forEach((element) => { + const elementDiv = document?.createElement('div'); + elementDiv.className = `element-${count}`; + elementDiv.id = `${rowDiv?.id}:element-${count}`; + count += 1; + + const elementStylesByClassName = { + padding: row?.spacing, + }; + + injectStylesheet?.injectWithAllowlist( + { + [`.${elementDiv?.className}`]: elementStylesByClassName, + }, + ALLOWED_MULTIPLE_FIELDS_STYLES, + ); + + const revealFrame = new RevealFrame(element, this.#context, + this.containerId, elementDiv); + this.revealFrameList?.push(revealFrame); + rowDiv?.append(elementDiv); + }); + + this.rootDiv?.append(rowDiv); + if (isComposableContainer) { + this.rootDiv?.append(errorTextElement); + } + }); + + if (this.#domForm) { + this.#domForm.innerHTML = ''; + document.body.innerHTML = ''; + this.#domForm?.append(this.rootDiv); + document?.body?.append(this.#domForm); + } + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.containerId, + data: { + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + + bus?.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window?.name, (data, callback) => { + callback?.({ + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }); + }); + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + + window?.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window?.name) { + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + } + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name) { + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + } + }); + }; +} diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts index d2c35e6b..c5691e4f 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -1,17 +1,35 @@ import injectStylesheet from 'inject-stylesheet'; import bus from 'framebus'; +import get from 'lodash/get'; import { getValueAndItsUnit, validateAndSetupGroupOptions } from '../../libs/element-options'; import { getFlexGridStyles } from '../../libs/styles'; import { ContainerType } from '../../skyflow'; -import { Context, Env, LogLevel } from '../../utils/common'; -import { getContainerType } from '../../utils/helpers'; +import { + Context, Env, LogLevel, + MessageType, +} from '../../utils/common'; +import { + fileValidation, generateUploadFileName, getContainerType, vaildateFileName, +} from '../../utils/helpers'; import { ALLOWED_MULTIPLE_FIELDS_STYLES, - ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ERROR_TEXT_STYLES, STYLE_TYPE, + COLLECT_TYPES, + ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, ERROR_TEXT_STYLES, STYLE_TYPE, } from '../constants'; import IFrameFormElement from './iframe-form'; import getCssClassesFromJss, { generateCssWithoutClass } from '../../libs/jss-styles'; import FrameElement from '.'; +import { + checkForElementMatchRule, checkForValueMatch, constructElementsInsertReq, + constructInsertRecordRequest, insertDataInCollect, + updateRecordsBySkyflowIDComposable, +} from '../../core-utils/collect'; +import SkyflowError from '../../libs/skyflow-error'; +import SKYFLOW_ERROR_CODE from '../../utils/constants'; +import Client from '../../client'; +import { printLog } from '../../utils/logs-helper'; + +const set = require('set-value'); export default class FrameElementInit { iframeFormElement: IFrameFormElement | undefined; @@ -30,9 +48,15 @@ export default class FrameElementInit { group: any; + frameList: FrameElement[] = []; + + iframeFormList: IFrameFormElement[] = []; + + #client!: Client; + constructor() { // this.createIframeElement(frameName, label, skyflowID, isRequired); - this.context = { logLevel: LogLevel.ERROR, env: Env.PROD }; // client level + this.context = { logLevel: LogLevel.INFO, env: Env.PROD }; // client level this.containerId = ''; this.#domForm = document.createElement('form'); this.#domForm.action = '#'; @@ -41,8 +65,603 @@ export default class FrameElementInit { }; this.updateGroupData(); this.createContainerDiv(this.group); + bus + // .target(this.clientMetaData.clientDomain) + .emit(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId, {}, (data: any) => { + data.client.config = { + ...data.client.config, + }; + this.#client = Client.fromJSON(data.client) as any; + }); + + window.addEventListener('message', this.handleCollectCall); } + private handleCollectCall = (event: MessageEvent) => { + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + if (inputElement.fieldType + === ELEMENTS.MULTI_FILE_INPUT.name) { + if (event?.data && event?.data?.name === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${inputElement.iFrameName}`) { + this.#client = Client.fromJSON(event?.data?.clientConfig); + this.multipleUploadFiles(inputElement, event?.data?.clientConfig, event?.data?.options) + ?.then((response: any) => { + window?.parent.postMessage({ + type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, + data: response, + }, this.clientMetaData?.clientDomain); + }).catch((error) => { + window?.parent.postMessage({ + type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, + data: error, + }, this.clientMetaData?.clientDomain); + }); + } + } + } + }); + + // if (event.origin === this.clientMetaData.clientDomain) { + if (event?.data && event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + + this.containerId) { + if (event?.data?.data && event?.data?.data?.type === COLLECT_TYPES.COLLECT) { + this.tokenize(event?.data?.data, event?.data?.clientConfig) + .then((response: any) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.containerId, + data: response, + }, this.clientMetaData?.clientDomain); + }) + .catch((error) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.containerId, + data: error, + }, this.clientMetaData?.clientDomain); + }); + } else if (event.data.data && event.data.data.type === COLLECT_TYPES.FILE_UPLOAD) { + this.parallelUploadFiles(event.data.data, event.data.clientConfig) + .then((response: any) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, + data: response, + }, this.clientMetaData?.clientDomain); + }) + .catch((error) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, + data: error, + }, this.clientMetaData?.clientDomain); + }); + } + } + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId) { + const data = event.data; + data.client.config = { + ...data.client.config, + }; + this.#client = Client.fromJSON(data.client) as any; + } + // } + }; + + private parallelUploadFiles = (options, config) => new Promise((rootResolve, rootReject) => { + const promises: Promise[] = []; + this.iframeFormList.forEach((inputElement) => { + let res: Promise; + if (inputElement) { + if ( + inputElement.fieldType + === ELEMENTS.FILE_INPUT.name + ) { + res = this.uploadFiles(inputElement, config); + promises.push(res); + } + } + }); + Promise.allSettled( + promises, + ).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + 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 { + const response = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response); + } + } + } else if (result.status === 'rejected') { + errorResponse.push(result.reason); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }); + + uploadFiles = (fileElement, clientConfig) => { + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); + if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); + const fileUploadObject: any = {}; + + const { + state, tableName, skyflowID, onFocusChange, preserveFileName, + } = fileElement; + + if (state.isRequired) { + onFocusChange(false); + } + try { + fileValidation(state.value, state.isRequired, fileElement); + } catch (err) { + return Promise.reject(err); + } + + const validatedFileState = fileValidation(state.value, state.isRequired, fileElement); + + if (!validatedFileState) { + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true)); + } + fileUploadObject[state.name] = state.value; + + const formData = new FormData(); + + const column = Object.keys(fileUploadObject)[0]; + + const value: Blob = Object.values(fileUploadObject)[0] as Blob; + + formData.append('columnName', column); + formData.append('tableName', tableName); + + if (preserveFileName) { + const isValidFileName = vaildateFileName(state.value.name); + if (!isValidFileName) { + return Promise.reject( + new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true), + ); + } + formData.append('file', value); + } else { + const generatedFileName = generateUploadFileName(state.value.name); + formData.append('file', new File([value], generatedFileName, { type: state.value.type })); + } + + if (skyflowID) { + formData.append('skyflowID', skyflowID); + } + + const client = this.#client; + const sendRequest = () => new Promise((rootResolve, rootReject) => { + client + .request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v2/vaults/${client.config.vaultID}/files/upload`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }) + .then((response: any) => { + rootResolve(response); + }) + .catch((error) => { + rootReject(error); + }); + }); + + return new Promise((resolve, reject) => { + sendRequest() + .then((res) => resolve(res)) + .catch((err) => { + reject(err); + }); + }); + }; + + private tokenize = (options, clientConfig: any) => { + let errorMessage = ''; + const insertRequestObject: any = {}; + const updateRequestObject: any = {}; + + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + if (inputElement) { + if ( + inputElement.fieldType + !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType + !== ELEMENTS.MULTI_FILE_INPUT.name + ) { + const { + // eslint-disable-next-line max-len + state, doesClientHasError, clientErrorText, errorText, onFocusChange, validations, + setValue, + } = inputElement; + if (state.isRequired || !state.isValid) { + onFocusChange(false); + } + if (validations + && checkForElementMatchRule(validations) + && checkForValueMatch(validations, inputElement)) { + setValue(state.value); + onFocusChange(false); + } + if (!state.isValid || !state.isComplete) { + if (doesClientHasError) { + errorMessage += `${state.name}:${clientErrorText}`; + } else { errorMessage += `${state.name}:${errorText} `; } + } + } + } + } + }); + + // return for error + if (errorMessage.length > 0) { + // eslint-disable-next-line max-len + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.COMPLETE_AND_VALID_INPUTS, [`${errorMessage}`], true)); + } + // eslint-disable-next-line consistent-return + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + const { + state, tableName, validations, skyflowID, + } = inputElement; + if (tableName) { + if ( + inputElement.fieldType + !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType + !== ELEMENTS.MULTI_FILE_INPUT.name + ) { + if ( + inputElement.fieldType + === ELEMENTS.checkbox.name + ) { + if (insertRequestObject[state.name]) { + insertRequestObject[state.name] = `${insertRequestObject[state.name]},${state.value + }`; + } else { + insertRequestObject[state.name] = state.value; + } + } else if (insertRequestObject[tableName] && !(skyflowID === '') && skyflowID === undefined) { + if (get(insertRequestObject[tableName], state.name) + && !(validations && checkForElementMatchRule(validations))) { + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT, + [state.name, tableName], true)); + } + set( + insertRequestObject[tableName], + state.name, + inputElement.getUnformattedValue(), + ); + } else if (skyflowID || skyflowID === '') { + if (skyflowID === '' || skyflowID === null) { + return Promise.reject(new SkyflowError( + SKYFLOW_ERROR_CODE.EMPTY_SKYFLOW_ID_IN_ADDITIONAL_FIELDS, + )); + } + if (updateRequestObject[skyflowID]) { + set( + updateRequestObject[skyflowID], + state.name, + inputElement.getUnformattedValue(), + ); + } else { + updateRequestObject[skyflowID] = {}; + set( + updateRequestObject[skyflowID], + state.name, + inputElement.getUnformattedValue(), + ); + set( + updateRequestObject[skyflowID], + 'table', + tableName, + ); + } + } else { + insertRequestObject[tableName] = {}; + set( + insertRequestObject[tableName], + state.name, + inputElement.getUnformattedValue(), + ); + } + } + } + } + }); + let finalInsertRequest; + let finalInsertRecords; + let finalUpdateRecords; + try { + [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq( + insertRequestObject, updateRequestObject, options.options, + ); + finalInsertRequest = constructInsertRecordRequest(finalInsertRecords, options.options); + } catch (error:any) { + return Promise.reject({ + error: error?.message, + }); + } + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); + const client = this.#client; + const sendRequest = () => new Promise((rootResolve, rootReject) => { + const insertPromiseSet: Promise[] = []; + + // const clientId = client.toJSON()?.metaData?.uuid || ''; + // getAccessToken(clientId).then((authToken) => { + if (finalInsertRequest.length !== 0) { + insertPromiseSet.push( + insertDataInCollect(finalInsertRequest, + client, options, finalInsertRecords, clientConfig.authToken as string), + ); + } + if (finalUpdateRecords.updateRecords.length !== 0) { + insertPromiseSet.push( + updateRecordsBySkyflowIDComposable( + finalUpdateRecords, client, options, clientConfig.authToken as string, + ), + ); + } + if (insertPromiseSet.length !== 0) { + Promise.allSettled(insertPromiseSet).then((resultSet: any) => { + const recordsResponse: any[] = []; + const errorsResponse: any[] = []; + + resultSet.forEach((result: + { status: string; value: any; reason?: any; }) => { + if (result.status === 'fulfilled') { + if (result.value.records !== undefined && Array.isArray(result.value.records)) { + result.value.records.forEach((record) => { + recordsResponse.push(record); + }); + } + if (result.value.errors !== undefined && Array.isArray(result.value.errors)) { + result.value.errors.forEach((error) => { + errorsResponse.push(error); + }); + } + } else { + if (result.reason?.records !== undefined && Array.isArray(result.reason?.records)) { + result.reason.records.forEach((record) => { + recordsResponse.push(record); + }); + } + if (result.reason?.errors !== undefined && Array.isArray(result.reason?.errors)) { + result.reason.errors.forEach((error) => { + errorsResponse.push(error); + }); + } + } + }); + if (errorsResponse.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse.length === 0) rootReject({ errors: errorsResponse }); + else rootReject({ records: recordsResponse, errors: errorsResponse }); + }); + } + // }).catch((err) => { + // rootReject({ + // error: err, + // }); + // }); + }); + + return new Promise((resolve, reject) => { + sendRequest() + .then((res) => resolve(res)) + .catch((err) => reject(err)); + }); + }; + + // eslint-disable-next-line consistent-return + private multipleUploadFiles = + (fileElement: IFrameFormElement, + clientConfig, metaData) => new Promise((rootResolve, rootReject) => { + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); + if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); + + const { + state, tableName, onFocusChange, preserveFileName, + } = fileElement; + if (state.isRequired) { + onFocusChange(false); + } + + if (state.value === undefined || state.value === null || state.value === '') { + rootReject({ error: 'No files selected' }); + return; + } + + const files = state.value instanceof FileList ? Array.from(state.value) : [state.value]; + this.validateFiles(files, state, fileElement); + + const uploadFile = (file: File, skyflowID?: string) => { + const formData = new FormData(); + formData.append('columnName', state.name); + if (tableName) formData.append('tableName', tableName); + + if (preserveFileName) { + formData.append('file', file); + } else { + const generatedFileName = generateUploadFileName(file.name); + formData.append('file', new File([file], generatedFileName, { type: file.type })); + } + if (skyflowID) formData.append('skyflowID', skyflowID); + const client = this.#client; + return this.#client.request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v2/vaults/${this.#client.config.vaultID}/files/upload`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }); + }; + + if (metaData && Object.keys(metaData).length > 0) { + const insertRequest = this.createInsertRequest(files.length, metaData); + this.insertDataCallInMultiFiles( + insertRequest, this.#client, tableName as string, clientConfig.authToken as string, + ).then((response: any) => { + const skyflowIDs = this.extractSkyflowIDs(response); + if (skyflowIDs.length === 0) { + rootReject({ error: 'No skyflow IDs returned from insert data' }); + return; + } + const promises = files.map((file, idx) => uploadFile(file, skyflowIDs[idx])); + Promise.allSettled(promises).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + 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 { + const response1 = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response1); + } + } + } else if (result.status === 'rejected') { + errorResponse.push({ error: result.reason }); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }).catch((error) => { + printLog(`${error}`, MessageType.LOG, this.context?.logLevel); + rootReject({ + error: error?.error || error, + }); + }); + } else { + const promises = files.map((file) => uploadFile(file)); + Promise.allSettled(promises).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + 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 { + const response1 = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response1); + } + } + } else if (result.status === 'rejected') { + errorResponse.push({ error: result.reason }); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + } + }); + + private validateFiles = (files: File[], state: any, fileElement: IFrameFormElement) => { + files.forEach((file) => { + // Check file validation + const validatedFileState = fileValidation(file, state.isRequired, fileElement); + if (!validatedFileState) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); + } + + // Check filename validation + const isValidFileName = vaildateFileName(file.name); + if (!isValidFileName) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true); + } + }); + return true; + }; + + private createInsertRequest = (numberOfRequests: number, options = {}) => { + // Create basic request structure + const request = { + records: [] as Array<{ fields: Record }>, + tokenization: false, + }; + + // Add empty field objects based on number of requests + for (let i = 0; i < numberOfRequests; i += 1) { + request.records.push({ + fields: options === undefined ? {} : options, + }); + } + + return request; + }; + + private extractSkyflowIDs = (response: { records: Array<{ skyflow_id: string }> }): string[] => { + if (!response?.records || !Array.isArray(response.records)) { + return []; + } + + return response.records + .map((record) => record.skyflow_id) + .filter((id) => id !== undefined && id !== null); + }; + + private insertDataCallInMultiFiles = ( + insertRequest, + client: Client, + tableName: string, + authToken: string, + ) => new Promise((rootResolve, rootReject) => { + client + .request({ + body: { + ...insertRequest, + }, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + .then((response: any) => { + // Extract skyflow IDs from response + const skyflowIDs = this.extractSkyflowIDs(response); + rootResolve({ + ...response, + skyflowIDs, // Add extracted IDs to response + }); + }) + .catch((error) => { + rootReject(error); + }); + }); + updateGroupData = () => { const frameName = window.name; const url = window.location?.href; @@ -56,7 +675,6 @@ export default class FrameElementInit { }; this.group = parsedRecord.record; this.containerId = parsedRecord.containerId; - bus .target(this.clientMetaData.clientDomain) .on(ELEMENT_EVENTS_TO_IFRAME.SET_VALUE + frameName, (data) => { @@ -73,6 +691,7 @@ export default class FrameElementInit { ...this.clientMetaData, isRequired, }, this.context, skyflowID); + this.iframeFormList.push(this.iframeFormElement); return this.iframeFormElement; }; @@ -184,11 +803,21 @@ export default class FrameElementInit { iFrameFormElement, element, elementDiv, + this.clientMetaData.clientDomain, ); + this.frameList.push(this.frameElement); + if (isComposableContainer && errorTextElement) { iFrameFormElement.on(ELEMENT_EVENTS_TO_CLIENT.BLUR, (state) => { errorTextMap[element.elementName] = state.error; this.#updateCombinedErrorText(errorTextElement.id, errorTextMap); + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, + data: { height: rootDiv.scrollHeight, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); }); } @@ -208,6 +837,24 @@ export default class FrameElementInit { bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window.name, (data, callback) => { callback({ height: rootDiv.scrollHeight, name: window.name }); }); + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, + data: { height: rootDiv.scrollHeight, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); + window.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window.name) { + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, + data: { height: rootDiv.scrollHeight, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); + } + }); }; #updateCombinedErrorText = (elementId, errorMessages) => { diff --git a/src/core/internal/iframe-form/index.ts b/src/core/internal/iframe-form/index.ts index 2ae70aac..1c2de7ba 100644 --- a/src/core/internal/iframe-form/index.ts +++ b/src/core/internal/iframe-form/index.ts @@ -113,7 +113,7 @@ export default class IFrameFormElement extends EventEmitter { blockEmptyFiles: boolean = false; - constructor(name: string, label: string, metaData, context: Context, skyflowID?: string) { + constructor(name: string, label: string, metaData: any, context: Context, skyflowID?: string) { super(); const frameValues = name.split(':'); const fieldType = frameValues[1]; @@ -219,6 +219,36 @@ export default class IFrameFormElement extends EventEmitter { : ELEMENT_EVENTS_TO_CLIENT.BLUR, value: { ...this.getStatus() }, }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: focus + ? ELEMENT_EVENTS_TO_CLIENT.FOCUS + : ELEMENT_EVENTS_TO_CLIENT.BLUR, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: focus + ? ELEMENT_EVENTS_TO_CLIENT.FOCUS + : ELEMENT_EVENTS_TO_CLIENT.BLUR, + name: this.iFrameName, + value: { ...this.getStatus() }, + }, + }, this.metaData.clientDomain); + } + } if (!focus) { bus.emit(ELEMENT_EVENTS_TO_CLIENT.BLUR + this.iFrameName); this._emit(ELEMENT_EVENTS_TO_CLIENT.BLUR, { @@ -282,6 +312,40 @@ export default class IFrameFormElement extends EventEmitter { this.mask = newMask; } + getFileDetails = (value: FileList | File | null): Array<{ + fileName: string; + fileSizeKB: number; + fileType: string; + }> => { + // Return empty array if no value + if (!value) return []; + + try { + // Handle FileList + if (value instanceof FileList) { + return Array.from(value).map((file) => ({ + fileName: file.name, + fileSizeKB: Math.ceil(file.size / 1024), + fileType: file.type, + })); + } + + // Handle single File + if (value instanceof File) { + return [{ + fileName: value.name, + fileSizeKB: Math.ceil(value.size / 1024), + fileType: value.type, + }]; + } + + // Return empty array for invalid input + return []; + } catch (error) { + return []; + } + }; + setValidation(validations: IValidationRule[] | undefined) { if (ELEMENTS[this.fieldType].regex) { this.regex = ELEMENTS[this.fieldType].regex; @@ -469,6 +533,21 @@ export default class IFrameFormElement extends EventEmitter { resp = false; } if (this.preserveFileName) vaildateFileNames = vaildateFileName(value.name); + } else if (this.fieldType === ElementType.MULTI_FILE_INPUT) { + const files = this.state.value instanceof FileList + ? Array.from(this.state.value) + : [this.state.value]; + for (let i = 0; i < files.length; i += 1) { + try { + resp = fileValidation(files[i], this.state.isRequired, { + allowedFileType: this.allowedFileType, + blockEmptyFiles: this.blockEmptyFiles, + }); + } catch (err) { + resp = false; + } + if (this.preserveFileName) vaildateFileNames = vaildateFileName(files[i].name); + } } else { // eslint-disable-next-line no-lonely-if if (this.regex && value) { @@ -591,6 +670,32 @@ export default class IFrameFormElement extends EventEmitter { event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } } else if ( data.options !== undefined @@ -673,23 +778,103 @@ export default class IFrameFormElement extends EventEmitter { event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } else if ( this.state.value && (this.fieldType === ELEMENTS.EXPIRATION_DATE.name || this.fieldType === ELEMENTS.EXPIRATION_MONTH.name - || this.fieldType === ELEMENTS.FILE_INPUT.name) + || this.fieldType === ELEMENTS.FILE_INPUT.name + || this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + ) ) { bus.emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, { name: this.iFrameName, event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } else if (!this.state.isEmpty) { bus.emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, { name: this.iFrameName, event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } this._emit(ELEMENT_EVENTS_TO_CLIENT.CHANGE, { diff --git a/src/core/internal/index.ts b/src/core/internal/index.ts index 8648eb8c..01878a53 100644 --- a/src/core/internal/index.ts +++ b/src/core/internal/index.ts @@ -89,15 +89,20 @@ export default class FrameElement { private selectedData?: number = undefined; + private clientDomain: string; + constructor( iFrameFormElement: IFrameFormElement, options: any, htmlDivElement: HTMLDivElement, + clientDomain: string = '', ) { + this.clientDomain = clientDomain; this.iFrameFormElement = iFrameFormElement; this.options = options; this.htmlDivElement = htmlDivElement; this.hasError = false; + this.mount(); this.iFrameFormElement.fieldName = options.column; this.iFrameFormElement.tableName = options.table; @@ -138,9 +143,16 @@ export default class FrameElement { this.inputParent = document.createElement('div'); this.inputParent.style.position = 'relative'; - const inputElement = document.createElement(type); this.domInput = inputElement; + inputElement.addEventListener('keydown', (event) => { + const keyboardEvent = event as KeyboardEvent; + if ((keyboardEvent.ctrlKey || keyboardEvent.metaKey) && keyboardEvent.key.toLowerCase() === 'z') { + keyboardEvent.preventDefault(); + this.setValue(''); + this.iFrameFormElement.setValue('', true); + } + }); this.domInput.iFrameFormElement = this.iFrameFormElement; inputElement.setAttribute(CUSTOM_ROW_ID_ATTRIBUTE, this.htmlDivElement?.id?.split(':')[0] || ''); this.inputParent.append(inputElement); @@ -160,6 +172,15 @@ export default class FrameElement { this.dropdownSelect = document.createElement('select'); this.dropdownSelect.setAttribute('style', this.options?.inputStyles?.dropdown ? (DROPDOWN_STYLES + styleToString(this.options.inputStyles.dropdown)) : DROPDOWN_STYLES); + this.dropdownSelect.addEventListener('focus', () => { + if (this.options?.inputStyles?.dropdownIcon?.focus) { + this.setDropdownIconStyle(this.options?.inputStyles?.dropdownIcon?.focus); + } + }); + + this.dropdownSelect.addEventListener('blur', () => { + this.setDropdownIconStyle(this.options?.inputStyles?.dropdownIcon); + }); this.dropdownSelect.addEventListener('change', (event:any) => { event.preventDefault(); @@ -217,6 +238,9 @@ export default class FrameElement { if (state.value && this.iFrameFormElement.fieldType === ELEMENTS.FILE_INPUT.name) { this.focusChange(false); } + if (state.value && this.iFrameFormElement.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + this.focusChange(false); + } this.focusChange(false); if (state.error && this.domError) { @@ -454,6 +478,12 @@ export default class FrameElement { .emit(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iFrameFormElement.iFrameName, { name: this.iFrameFormElement.iFrameName, }); + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iFrameFormElement.iFrameName, + data: { + name: this.iFrameFormElement.iFrameName, + }, + }, this.clientDomain); this.updateStyleClasses(this.iFrameFormElement.getStatus()); }; @@ -567,6 +597,10 @@ export default class FrameElement { const target = event.target as HTMLFormElement; this.iFrameFormElement.setValue(target.files[0], target.checkValidity()); this.focusChange(true); + } else if (this.iFrameFormElement.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + const target = event.target as HTMLFormElement; + this.iFrameFormElement.setValue(target.files, target.checkValidity()); + this.focusChange(true); } else { const target = event.target as HTMLInputElement; const { mask } = this.iFrameFormElement; @@ -817,11 +851,21 @@ export default class FrameElement { }; onSubmit = () => { - bus - .emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameFormElement.iFrameName, { - name: this.iFrameFormElement.iFrameName, - event: ELEMENT_EVENTS_TO_CLIENT.SUBMIT, - }); + if (this.iFrameFormElement.containerType === ContainerType.COMPOSABLE) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameFormElement.iFrameName, + data: { + name: this.iFrameFormElement.iFrameName, + event: ELEMENT_EVENTS_TO_CLIENT.SUBMIT, + }, + }, this.clientDomain); + } else { + bus + .emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameFormElement.iFrameName, { + name: this.iFrameFormElement.iFrameName, + event: ELEMENT_EVENTS_TO_CLIENT.SUBMIT, + }); + } }; onArrowKeys = (keyBoardEvent: KeyboardEvent) => { @@ -861,7 +905,6 @@ export default class FrameElement { case INPUT_KEYBOARD_EVENTS.ENTER: this.onSubmit(); - keyBoardEvent.preventDefault(); break; default: break; @@ -1138,4 +1181,16 @@ export default class FrameElement { } } } + + private setDropdownIconStyle(styleObj?: any) { + if (this.dropdownIcon?.style.display === 'block') { + this.dropdownIcon.setAttribute( + 'style', + styleObj + ? DROPDOWN_ICON_STYLES + styleToString(styleObj) + : DROPDOWN_ICON_STYLES, + ); + this.dropdownIcon.style.display = 'block'; + } + } } diff --git a/src/core/internal/internal-types/index.ts b/src/core/internal/internal-types/index.ts index bb236b3f..6b5d08aa 100644 --- a/src/core/internal/internal-types/index.ts +++ b/src/core/internal/internal-types/index.ts @@ -44,9 +44,11 @@ export interface RevealContainerProps { containerId: string; isMounted: boolean; eventEmitter: EventEmitter; + type: string; } export interface InternalState { + metaData: any; isEmpty: boolean, isValid: boolean, isFocused: boolean, @@ -80,4 +82,5 @@ export interface Metadata extends ClientMetadata { clientJSON: ClientToJSON; containerType: ContainerType; skyflowContainer: SkyflowContainer; + getSkyflowBearerToken: () => Promise; } diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 5611c51b..973e0a2a 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -15,17 +15,25 @@ import { RENDER_ELEMENT_IMAGE_STYLES, DEFAULT_FILE_RENDER_ERROR, ELEMENT_EVENTS_TO_CLIENT, + REVEAL_TYPES, } from '../../constants'; import getCssClassesFromJss, { generateCssWithoutClass } from '../../../libs/jss-styles'; import { printLog, parameterizedString, } from '../../../utils/logs-helper'; import logs from '../../../utils/logs'; -import { Context, MessageType } from '../../../utils/common'; +import { + Context, IRenderResponseType, IRevealRecord, MessageType, +} from '../../../utils/common'; import { constructMaskTranslation, - getAtobValue, getMaskedOutput, getValueFromName, handleCopyIconClick, styleToString, + formatRevealElementOptions, + getAtobValue, + getMaskedOutput, getValueFromName, handleCopyIconClick, styleToString, } from '../../../utils/helpers'; +import { formatForRenderClient, getFileURLFromVaultBySkyflowIDComposable } from '../../../core-utils/reveal'; +import Client from '../../../client'; +import properties from '../../../properties'; const { getType } = require('mime'); @@ -65,6 +73,8 @@ class RevealFrame { #skyflowContainerId: string = ''; + #client!: Client; + static init() { const url = window.location?.href; const configIndex = url.indexOf('?'); @@ -75,9 +85,9 @@ class RevealFrame { parsedRecord.context, skyflowContainerId); } - constructor(record, context, id) { + constructor(record, context: Context, id: string, rootDiv?: HTMLDivElement) { this.#skyflowContainerId = id; - this.#name = window.name; + this.#name = rootDiv ? record?.name : window.name; this.#containerId = getValueFromName(this.#name, 2); const encodedClientDomain = getValueFromName(this.#name, 4); const clientDomain = getAtobValue(encodedClientDomain); @@ -91,14 +101,14 @@ class RevealFrame { getCssClassesFromJss(REVEAL_ELEMENT_DIV_STYLE, 'div'); this.#labelElement = document.createElement('span'); - this.#labelElement.className = `SkyflowElement-label-${STYLE_TYPE.BASE}`; + this.#labelElement.className = `SkyflowElement-${this.#name}-label-${STYLE_TYPE.BASE}`; this.#dataElememt = document.createElement('span'); - this.#dataElememt.className = `SkyflowElement-content-${STYLE_TYPE.BASE}`; + this.#dataElememt.className = `SkyflowElement-${this.#name}-content-${STYLE_TYPE.BASE}`; this.#dataElememt.id = this.#name; this.#errorElement = document.createElement('span'); - this.#errorElement.className = `SkyflowElement-error-${STYLE_TYPE.BASE}`; + this.#errorElement.className = `SkyflowElement-${this.#name}-error-${STYLE_TYPE.BASE}`; if (this.#record.enableCopy) { this.domCopy = document.createElement('img'); @@ -125,13 +135,14 @@ class RevealFrame { ...REVEAL_ELEMENT_LABEL_DEFAULT_STYLES[STYLE_TYPE.BASE], ...this.#record.labelStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#labelStyles, 'label'); + // getCssClassesFromJss(this.#labelStyles, 'label'); + getCssClassesFromJss(this.#labelStyles, `${this.#name}-label`); if (this.#record.labelStyles[STYLE_TYPE.GLOBAL]) { generateCssWithoutClass(this.#record.labelStyles[STYLE_TYPE.GLOBAL]); } } else { - getCssClassesFromJss(REVEAL_ELEMENT_LABEL_DEFAULT_STYLES, 'label'); + getCssClassesFromJss(REVEAL_ELEMENT_LABEL_DEFAULT_STYLES, `${this.#name}-label`); } } this.updateDataView(); @@ -140,7 +151,7 @@ class RevealFrame { this.#inputStyles[STYLE_TYPE.BASE] = { ...this.#record.inputStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#inputStyles, 'content'); + getCssClassesFromJss(this.#inputStyles, `${this.#name}-content`); if (this.#record.inputStyles[STYLE_TYPE.GLOBAL]) { generateCssWithoutClass(this.#record.inputStyles[STYLE_TYPE.GLOBAL]); } @@ -155,26 +166,68 @@ class RevealFrame { ...REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES[STYLE_TYPE.BASE], ...this.#record.errorTextStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#errorTextStyles, 'error'); + getCssClassesFromJss(this.#errorTextStyles, `${this.#name}-error`); if (this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]) { generateCssWithoutClass(this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]); } } else { getCssClassesFromJss( REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES, - 'error', + `${this.#name}-error`, ); } this.#elementContainer.appendChild(this.#dataElememt); - document.body.append(this.#elementContainer); + if (rootDiv) rootDiv.append(this.#elementContainer); + else document.body.append(this.#elementContainer); + // document.body.append(this.#elementContainer); bus.emit(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#name, { name: this.#name }); - + if (rootDiv) { + this.getConfig(); + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#name, + data: { + name: this.#name, + }, + }, this.#clientDomain); + + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + data: { height: this.#elementContainer.scrollHeight, name: this.#name }, + }, this.#clientDomain); + } bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, (_, callback) => { callback({ height: this.#elementContainer.scrollHeight, name: this.#name }); }); + const sub2 = (responseUrl) => { + if (responseUrl.iframeName === this.#name) { + if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { + this.setRevealError(DEFAULT_FILE_RENDER_ERROR); + if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { + this.#dataElememt.innerText = this.#record.altText; + } + bus + .emit( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + { + height: this.#elementContainer.scrollHeight, + }, () => { + }, + ); + } else { + const ext = this.getExtension(responseUrl.url); + this.addFileRender(responseUrl.url, ext); + } + } + }; + bus + .target(window.location.origin) + .on( + ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + this.#name, + sub2, + ); const sub = (data) => { if (Object.prototype.hasOwnProperty.call(data, this.#record.token)) { @@ -189,7 +242,7 @@ class RevealFrame { this.#dataElememt.innerText = formattedOutput; } printLog(parameterizedString(logs.infoLogs.ELEMENT_REVEALED, - CLASS_NAME, this.#record.token), MessageType.LOG, this.#context.logLevel); + CLASS_NAME, this.#record.token), MessageType.LOG, this.#context?.logLevel); // bus // .target(window.location.origin) @@ -220,39 +273,180 @@ class RevealFrame { if (data.isTriggerError) { this.setRevealError(data.clientErrorText as string); } else { this.setRevealError(''); } } }); + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + this.#name, + data: { + name: window.name, + }, + }, this.#clientDomain, + ); this.updateRevealElementOptions(); - - const sub2 = (responseUrl) => { - if (responseUrl.iframeName === this.#name) { - if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { - this.setRevealError(DEFAULT_FILE_RENDER_ERROR); - if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { - this.#dataElememt.innerText = this.#record.altText; - } - bus - .emit( - ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - { - height: this.#elementContainer.scrollHeight, - }, () => { - }, + window.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.#name) { + if (event?.data?.data?.iframeName === this.#name + && event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + this.renderFile(this.#record, event?.data?.clientConfig)?.then((resolvedResult) => { + const result = formatForRenderClient( + resolvedResult as IRenderResponseType, + this.#record?.column, ); - } else { - const ext = this.getExtension(responseUrl.url); - this.addFileRender(responseUrl.url, ext); + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result, + }, + }, this.#clientDomain); + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + })?.catch((error) => { + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result: { + errors: error, + }, + }, + }, this.#clientDomain); + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + }); } } - }; - bus - .target(window.location.origin) - .on( - ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + this.#name, - sub2, + + if (event?.data?.type === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name) { + if (event?.data?.data?.height) { + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + data: { + height: this.#elementContainer?.scrollHeight ?? 0, + name: this.#name, + }, + }, this.#clientDomain); + } + } + }); + } + + responseUpdate = (data) => { + if (data?.frameId === this.#record?.name && data?.error) { + if (!Object.prototype.hasOwnProperty.call(this.#record, 'skyflowID')) { + this.setRevealError(REVEAL_ELEMENT_ERROR_TEXT); + } + } else if (data?.frameId === this.#record?.name + && data?.[0]?.token + && this.#record?.token === data?.[0]?.token) { + const responseValue = data?.[0]?.value as string ?? ''; + this.#revealedValue = responseValue; + this.isRevealCalled = true; + this.#dataElememt.innerText = responseValue; + + if (this.#record?.mask) { + const { formattedOutput } = getMaskedOutput( + this.#dataElememt?.innerText ?? '', + this.#record?.mask?.[0], + constructMaskTranslation(this.#record?.mask), + ); + this.#dataElememt.innerText = formattedOutput ?? ''; + } + + printLog( + parameterizedString( + logs?.infoLogs?.ELEMENT_REVEALED, + CLASS_NAME, + this.#record?.token, + ), + MessageType.LOG, + this.#context?.logLevel, ); + } + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + data: { + height: this.#elementContainer?.scrollHeight ?? 0, + name: this.#name, + }, + }, + this.#clientDomain, + ); + }; + + getConfig = () => { + const url = window.location?.href; + const configIndex = url.indexOf('?'); + const encodedString = configIndex !== -1 ? decodeURIComponent(url.substring(configIndex + 1)) : ''; + const parsedRecord = encodedString ? JSON.parse(atob(encodedString)) : {}; + this.#clientDomain = parsedRecord.clientDomain || ''; + this.#containerId = parsedRecord.containerId; + }; + + getData = () => this.#record; + + private sub2 = (responseUrl: { iframeName?: string; error?: string; url?: string }) => { + if (responseUrl.iframeName === this.#name) { + if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { + this.setRevealError(DEFAULT_FILE_RENDER_ERROR); + if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { + this.#dataElememt.innerText = this.#record.altText; + } + bus + .emit( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + { + height: this.#elementContainer.scrollHeight, + }, () => { + }, + ); + } else { + const ext = this.getExtension(responseUrl.url as string); + this.addFileRender(responseUrl.url as string, ext); + } + } + }; + + private renderFile(data: IRevealRecord, clientConfig): + Promise | undefined { + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); + return new Promise((resolve, reject) => { + try { + getFileURLFromVaultBySkyflowIDComposable(data, this.#client, clientConfig.authToken) + .then((resolvedResult) => { + let url = ''; + if (resolvedResult.fields && data.column) { + url = resolvedResult.fields[data.column]; + } + this.sub2({ + url, + iframeName: this.#name, + }); + resolve(resolvedResult); + }, + (rejectedResult) => { + this.sub2({ + error: DEFAULT_FILE_RENDER_ERROR, + iframeName: this.#name, + }); + reject(rejectedResult); + }); + } catch (err) { + reject(err); + } + }); } // eslint-disable-next-line class-methods-use-this - private getExtension(url) { + private getExtension(url: string) { try { const params = new URL(url).searchParams; const name = params.get('response-content-disposition'); @@ -266,7 +460,7 @@ class RevealFrame { } } - private addFileRender(responseUrl, ext) { + private addFileRender(responseUrl: string, ext: string) { let tag = ''; if (typeof ext === 'string' && ext.includes('image')) { tag = 'img'; @@ -312,6 +506,31 @@ class RevealFrame { } private updateRevealElementOptions() { + window.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + this.#name) { + const data = event?.data; + if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS) { + const updatedValue = data.updatedValue as object; + this.#record = { + ...this.#record, + ...updatedValue, + ...formatRevealElementOptions(updatedValue), + }; + this.updateElementProps(); + if (this.isRevealCalled) { + if (this.#record?.mask) { + const { formattedOutput } = getMaskedOutput( + this.#revealedValue ?? '', + this.#record?.mask?.[0], + constructMaskTranslation(this.#record?.mask), + ); + this.#dataElememt.innerText = formattedOutput ?? ''; + } + } + } + } + }); bus .target(this.#clientDomain) .on(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#name, (data) => { @@ -355,7 +574,7 @@ class RevealFrame { ...this.#inputStyles, ...this.#record.inputStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#inputStyles, 'content'); + getCssClassesFromJss(this.#inputStyles, `${this.#name}-content`); if (this.#record.inputStyles[STYLE_TYPE.GLOBAL]) { const newInputGlobalStyles = { ...this.#inputStyles[STYLE_TYPE.GLOBAL], @@ -370,7 +589,7 @@ class RevealFrame { ...REVEAL_ELEMENT_LABEL_DEFAULT_STYLES[STYLE_TYPE.BASE], ...this.#record.labelStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#labelStyles, 'label'); + getCssClassesFromJss(this.#labelStyles, `${this.#name}-label`); if (this.#record.labelStyles[STYLE_TYPE.GLOBAL]) { const newLabelGlobalStyles = { @@ -386,7 +605,7 @@ class RevealFrame { ...this.#errorTextStyles[STYLE_TYPE.BASE], ...this.#record.errorTextStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#errorTextStyles, 'error'); + getCssClassesFromJss(this.#errorTextStyles, `${this.#name}-error`); if (this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]) { const newErrorTextGlobalStyles = { ...this.#errorTextStyles[STYLE_TYPE.GLOBAL], diff --git a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts index 2d63c84c..6de070bf 100644 --- a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts +++ b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts @@ -111,11 +111,11 @@ class SkyflowFrameController { try { window.CoralogixRum.info(SDK_IFRAME_EVENT, data.event); printLog(parameterizedString(logs.infoLogs.METRIC_CAPTURE_EVENT), - MessageType.LOG, this.#context.logLevel); + MessageType.LOG, this.#context?.logLevel); } catch (err: any) { printLog(parameterizedString(logs.infoLogs.UNKNOWN_METRIC_CAPTURE_EVENT, err.toString()), - MessageType.LOG, this.#context.logLevel); + MessageType.LOG, this.#context?.logLevel); } } }, @@ -569,7 +569,8 @@ class SkyflowFrameController { if (inputElement) { if ( inputElement.iFrameFormElement.fieldType - !== ELEMENTS.FILE_INPUT.name + !== ELEMENTS.FILE_INPUT.name && inputElement.iFrameFormElement.fieldType + !== ELEMENTS.MULTI_FILE_INPUT.name ) { const { state, doesClientHasError, clientErrorText, errorText, onFocusChange, validations, @@ -609,6 +610,7 @@ class SkyflowFrameController { if ( inputElement.iFrameFormElement.fieldType !== ELEMENTS.FILE_INPUT.name + && inputElement.iFrameFormElement.fieldType !== ELEMENTS.MULTI_FILE_INPUT.name ) { if ( inputElement.iFrameFormElement.fieldType diff --git a/src/index-internal.ts b/src/index-internal.ts index 31274800..1cf7049f 100644 --- a/src/index-internal.ts +++ b/src/index-internal.ts @@ -4,6 +4,7 @@ Copyright (c) 2022 Skyflow, Inc. import 'core-js/stable'; import RevealFrame from './core/internal/reveal/reveal-frame'; import { + COMPOSABLE_REVEAL, FRAME_ELEMENT, FRAME_REVEAL, SKYFLOW_FRAME_CONTROLLER, @@ -18,6 +19,7 @@ import { } from './utils/logs-helper'; import { getAtobValue, getValueFromName } from './utils/helpers'; import FrameElementInit from './core/internal/frame-element-init'; +import RevealComposableFrameElementInit from './core/internal/composable-frame-element-init'; (function init(root: any) { try { @@ -26,6 +28,9 @@ import FrameElementInit from './core/internal/frame-element-init'; const frameId = getValueFromName(frameName, 1); if (frameType === SKYFLOW_FRAME_CONTROLLER) { SkyflowFrameController.init(frameId); + } else if (frameType === COMPOSABLE_REVEAL) { + root.Skyflow = RevealComposableFrameElementInit; + RevealComposableFrameElementInit.startFrameElement(); } else if (frameType === FRAME_ELEMENT) { const logLevel = getValueFromName(frameName, 4) || LogLevel.ERROR; printLog( diff --git a/src/index-node.ts b/src/index-node.ts index c5a59b1b..380c2234 100644 --- a/src/index-node.ts +++ b/src/index-node.ts @@ -71,5 +71,6 @@ export { default as ComposableElement } from './core/external/collect/compose-co export { default as RevealContainer } from './core/external/reveal/reveal-container'; export { default as RevealElement } from './core/external/reveal/reveal-element'; export { default as ThreeDS } from './core/external/threeds/threeds'; - +export { default as ComposableRevealContainer } from './core/external/reveal/composable-reveal-container'; +export { default as ComposableRevealElement } from './core/external/reveal/composable-reveal-element'; export default Skyflow; diff --git a/src/libs/element-options.ts b/src/libs/element-options.ts index 360c6258..df162c82 100644 --- a/src/libs/element-options.ts +++ b/src/libs/element-options.ts @@ -380,7 +380,7 @@ export const formatOptions = ( break; } - case ELEMENTS.FILE_INPUT.name: { + case ELEMENTS.FILE_INPUT.name || ELEMENTS.MULTI_FILE_INPUT.name: { if (!Object.prototype.hasOwnProperty.call(formattedOptions, 'preserveFileName')) { formattedOptions = { ...formattedOptions, preserveFileName: true }; } @@ -402,7 +402,7 @@ export const formatOptions = ( if (Object.prototype.hasOwnProperty.call(formattedOptions, 'preserveFileName') && !validateBooleanOptions(formattedOptions.preserveFileName)) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_BOOLEAN_OPTIONS, ['preserveFileName'], true); } - if (elementType === ELEMENTS.FILE_INPUT.name) { + if (elementType === ELEMENTS.FILE_INPUT.name || elementType === ELEMENTS.MULTI_FILE_INPUT.name) { if (options.allowedFileType) { if (!Array.isArray(options.allowedFileType)) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ALLOWED_OPTIONS, [], true); diff --git a/src/skyflow.ts b/src/skyflow.ts index afd84a6f..b3350ee2 100644 --- a/src/skyflow.ts +++ b/src/skyflow.ts @@ -51,11 +51,13 @@ import ComposableContainer from './core/external/collect/compose-collect-contain import { validateComposableContainerOptions } from './utils/validators'; import ThreeDS from './core/external/threeds/threeds'; import { ClientMetadata, SkyflowElementProps } from './core/internal/internal-types'; +import ComposableRevealContainer from './core/external/reveal/composable-reveal-container'; export enum ContainerType { COLLECT = 'COLLECT', REVEAL = 'REVEAL', COMPOSABLE = 'COMPOSABLE', + COMPOSE_REVEAL = 'COMPOSABLE_REVEAL', } export interface SkyflowConfigOptions { logLevel?: LogLevel; @@ -176,9 +178,50 @@ class Skyflow { return skyflow; } + #getSkyflowBearerToken: () => Promise = () => new Promise((resolve, reject) => { + if ( + this.#client.config.getBearerToken + && (!this.#bearerToken || !isTokenValid(this.#bearerToken)) + ) { + this.#client.config + .getBearerToken() + .then((bearerToken) => { + if (isTokenValid(bearerToken)) { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#logLevel); + this.#bearerToken = bearerToken; + resolve(this.#bearerToken); + } else { + printLog(parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), MessageType.ERROR, this.#logLevel); + reject({ + error: parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), + }); + } + }) + .catch((err) => { + printLog(parameterizedString(logs.errorLogs.BEARER_TOKEN_REJECTED), MessageType.ERROR, + this.#logLevel); + reject({ error: err }); + }); + } else { + printLog(parameterizedString(logs.infoLogs.REUSE_BEARER_TOKEN, CLASS_NAME), + MessageType.LOG, + this.#logLevel); + resolve(this.#bearerToken); + } + }); + container(type: ContainerType.COLLECT, options?: ContainerOptions): CollectContainer; container(type: ContainerType.COMPOSABLE, options?: ContainerOptions): ComposableContainer; container(type: ContainerType.REVEAL, options?: ContainerOptions): RevealContainer; + container(type: ContainerType.COMPOSE_REVEAL, + options?: ContainerOptions) + : ComposableRevealContainer; container(type: ContainerType, options?: ContainerOptions) { switch (type) { case ContainerType.COLLECT: { @@ -187,6 +230,7 @@ class Skyflow { clientJSON: this.#client.toJSON(), containerType: type, skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, }, this.#skyflowElements, { logLevel: this.#logLevel, env: this.#env }, options); @@ -201,6 +245,7 @@ class Skyflow { clientJSON: this.#client.toJSON(), containerType: type, skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, }, this.#skyflowElements, { logLevel: this.#logLevel, env: this.#env }, options); @@ -211,23 +256,38 @@ class Skyflow { } case ContainerType.COMPOSABLE: { 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!, - ); + const composableContainer = new ComposableContainer({ + ...this.#metadata, + clientJSON: this.#client.toJSON(), + containerType: type, + skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, + }, + this.#skyflowElements, + { logLevel: this.#logLevel, env: this.#env }, options!); printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), MessageType.LOG, this.#logLevel); return composableContainer; } + case ContainerType.COMPOSE_REVEAL: { + validateComposableContainerOptions(options!); + const revealComposableContainer = new ComposableRevealContainer({ + ...this.#metadata, + clientJSON: this.#client.toJSON(), + containerType: type, + skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, + }, + this.#skyflowElements, + { logLevel: this.#logLevel, env: this.#env }, options); + printLog(parameterizedString(logs.infoLogs.REVEAL_CONTAINER_CREATED, CLASS_NAME), + MessageType.LOG, + this.#logLevel); + return revealComposableContainer; + } + default: if (!type) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_CONTAINER_TYPE, [], true); diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index d0e330ea..32dcdb4a 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -85,6 +85,15 @@ export interface IRevealRecord { table?: string; } +export interface IRevealRecordComposable { + token?: string; + redaction?: RedactionType; + column?: string; + skyflowID?: string; + table?: string; + iframeName?: string; +} + export interface IInsertResponse { records: IInsertResponseReocrds[]; } @@ -100,6 +109,7 @@ export interface IRevealResponseType { export interface IRenderResponseType { fields?: Record errors?: Record + fileMetadata?: Record } export interface IDetokenizeInput { @@ -328,6 +338,14 @@ export interface ICollectOptions { additionalFields?: IInsertRecordInput, upsert?: Array, } +export interface MetaData { + [key: string]: any, +} +export interface EventConfig{ + authToken: string, + vaultURL: string, + vaultID: string, +} export interface UploadFilesResponse { fileUploadResponse?: Record, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 7cf87fda..2c5a3138 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -4,6 +4,12 @@ Copyright (c) 2022 Skyflow, Inc. import logs from './logs'; const SKYFLOW_ERROR_CODE = { + MULTI_FILE_NOT_SUPPORTED: { + code: 400, + description: logs.errorLogs.MULTI_FILE_NOT_SUPPORTED, + }, + INVALID_REVEAL_COMPOSABLE_INPUT: + { code: 400, description: logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT }, INVALID_FILE_NAME: { code: 400, description: logs.errorLogs.INVALID_FILE_NAME }, INVALID_FIELD: { code: 400, description: logs.errorLogs.INVALID_FIELD }, VAULTID_IS_REQUIRED: { code: 400, description: logs.errorLogs.VAULTID_IS_REQUIRED }, diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 6f86f350..58fa0a43 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -231,6 +231,9 @@ export const styleToString = (style) => Object.keys(style).reduce((acc, key) => export const getContainerType = (frameName:string):ContainerType => { const frameNameParts = frameName.split(':'); + if (frameNameParts[0] === 'reveal-composable') { + return ContainerType.COMPOSE_REVEAL; + } return (frameNameParts[1] === 'group') ? ContainerType.COMPOSABLE : ContainerType.COLLECT; diff --git a/src/utils/logs.ts b/src/utils/logs.ts index d1c18b55..648407ba 100644 --- a/src/utils/logs.ts +++ b/src/utils/logs.ts @@ -10,6 +10,8 @@ const logs = { CREATE_COLLECT_CONTAINER: '%s1 - Creating Collect container.', COLLECT_CONTAINER_CREATED: '%s1 - Created Collect container successfully.', + INITIALIZE_COMPOSABLE_CLIENT: '%s1 - Initializing Composable container.', + CREATE_REVEAL_CONTAINER: '%s1 - Creating Reveal container.', REVEAL_CONTAINER_CREATED: '%s1 - Created Reveal container successfully.', @@ -26,6 +28,8 @@ const logs = { ELEMENT_REVEALED: '%s1 - %s2 Element revealed.', FILE_RENDERED: '%s1 - %s2 File rendered.', COLLECT_SUBMIT_SUCCESS: '%s1 - Data has been collected successfully.', + UPLOAD_FILES_SUCCESS: '%s1 - Files uploaded successfully.', + MULTI_UPLOAD_FILES_SUCCESS: '%s1 - Multiple files uploaded successfully.', REVEAL_SUBMIT_SUCCESS: '%s1 - Data has been revealed successfully.', RENDER_SUBMIT_SUCCESS: '%s1 - File download URL has been fetched successfully.', INSERT_DATA_SUCCESS: '%s1 - Data has been inserted successfully.', @@ -93,6 +97,8 @@ const logs = { VALIDATE_GET_BY_ID_INPUT: '%s1 - Validating getByID input.', }, errorLogs: { + MULTI_FILE_NOT_SUPPORTED: 'Multi file upload is only supported in MULT_FILE_INPUT element in composable container. Please use MULT_FILE_INPUT element for multi file upload.', + INVALID_REVEAL_COMPOSABLE_INPUT: 'Reveal composable input is invalid. Please provide a valid input.', NO_ELEMENTS_IN_COLLECT: 'Validation error. No elements found in collect container', NO_ELEMENTS_IN_COMPOSABLE: 'Validation error. No elements found in composable container', NO_ELEMENTS_IN_REVEAL: 'Validation error. No elements found in reveal container', diff --git a/src/utils/validators/index.ts b/src/utils/validators/index.ts index 82efcd45..f4ce7a88 100644 --- a/src/utils/validators/index.ts +++ b/src/utils/validators/index.ts @@ -7,7 +7,6 @@ import { ALLOWED_EXPIRY_YEAR_FORMATS, CardType, CARD_TYPE_REGEX, DEFAULT_CARD_LENGTH_RANGE, - ElementType, } from '../../core/constants'; import { IRevealElementInput } from '../../core/external/reveal/reveal-container'; import SkyflowError from '../../libs/skyflow-error'; @@ -604,10 +603,6 @@ export const validateCollectElementInput = (input: CollectElementInput, logLevel if (Object.prototype.hasOwnProperty.call(input, 'skyflowID') && !(typeof input.skyflowID === 'string')) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_SKYFLOWID_IN_COLLECT, [], true); } - if (input.type === ElementType.FILE_INPUT - && !Object.keys(input).includes('skyflowID')) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_SKYFLOWID_IN_COLLECT, [], true); - } }; export const validateUpsertOptions = (upsertOptions) => { diff --git a/tests/core-utils/reveal.test.js b/tests/core-utils/reveal.test.js index 5ced5b97..1e45bb22 100644 --- a/tests/core-utils/reveal.test.js +++ b/tests/core-utils/reveal.test.js @@ -2,9 +2,11 @@ Copyright (c) 2022 Skyflow, Inc. */ import Skyflow from '../../src/skyflow'; -import {formatRecordsForClient,formatRecordsForIframe, formatRecordsForRender, formatForRenderClient, getFileURLFromVaultBySkyflowID, getFileURLForRender} from "../../src/core-utils/reveal"; +import {formatRecordsForClient, formatRecordsForClientComposable, formatRecordsForIframe, formatRecordsForRender, formatForRenderClient, getFileURLFromVaultBySkyflowID, getFileURLForRender, getFileURLFromVaultBySkyflowIDComposable, fetchRecordsByTokenIdComposable} from "../../src/core-utils/reveal"; import { Env, LogLevel } from '../../src/utils/common'; +import { getAccessToken } from '../../src/utils/bus-events'; import Client from '../../src/client'; +import { url } from 'inspector'; const testTokenId = '1677f7bd-c087-4645-b7da-80a6fd1a81a4'; const testInvalidTokenId = '80a6fd1a81a4-b7da-c087-4645'; @@ -57,7 +59,11 @@ const skyflow = Skyflow.init({ }); jest.setTimeout(15000); - +jest.mock('../../src/utils/bus-events', () => ({ + getAccessToken: jest.fn( + () => Promise.resolve('mockAccessToken') + ), +})); describe('Reveal PureJs- get() Method Input', () => { test('should throw error for Empty Input Array', (done) => { skyflow.detokenize([]).catch((err) => { @@ -82,19 +88,7 @@ describe('Reveal PureJs- get() Method Input', () => { expect(err).toBeDefined(); done(); }); - }); - // test('should throw error for Missing redaction Property', (done) => { - // skyflow.detokenize({ - // records: [ - // { - // token: testTokenId, - // }, - // ], - // }).catch((err) => { - // expect(err).toBeDefined(); - // done(); - // }); - // }); + }); test('should throw error for Empty string in id value', (done) => { skyflow.detokenize({ records: [ @@ -129,6 +123,29 @@ describe("formatRecordsForClient fn test",()=>{ expect(fnResponse.success).toBeUndefined(); }); }); +describe("formatRecordsForClientComposable fn test",()=>{ + test("only success records",()=>{ + const testInput = {"records":[{"token":"7402-2242-2342-232","value":"231", "valueType" : "STRING"}] } + const fnResponse = formatRecordsForClientComposable(testInput, {"7402-2242-2342-232": "231"}); + expect(fnResponse.success.length).toBe(1); + expect(fnResponse.errors).toBeUndefined(); + }); + test("both success and error records",()=>{ + const testInput = {"records":[{"token":"7402-2242-2342-232","value":"231", "valueType" : "STRING"}],"errors":[{"token":"3232-6434-3253-4221"}]}; + const fnResponse = formatRecordsForClientComposable(testInput,{"7402-2242-2342-232": "231"}); + expect(fnResponse.errors.length).toBe(1); + expect(fnResponse.success.length).toBe(1); + }); + test("only error records",()=>{ + const testInput = {"errors":[{ + "token":"3232-6434-3253-4221", + "error":"token not found" + }]}; + const fnResponse = formatRecordsForClientComposable(testInput); + expect(fnResponse.errors.length).toBe(1); + expect(fnResponse.success).toBeUndefined(); + }); +}); describe("formatRecordsForIframe fn test",()=>{ test("no records should return empty object",()=>{ @@ -164,7 +181,7 @@ describe("formatRecordsForRender fn test",()=>{ "url": "http://dummy.com",}); }); }); -describe("formatRecordsForIframe fn test",()=>{ +describe("formatForRenderClient fn test",()=>{ test("no records should return empty object",()=>{ const testInput = {}; const fnResponse = formatForRenderClient(testInput, 'col'); @@ -178,9 +195,27 @@ describe("formatRecordsForIframe fn test",()=>{ const fnResponse = formatForRenderClient(testInput, 'col'); expect(fnResponse).toStrictEqual({ success :{"column": "col", "skyflow_id": "id", + "fileMetadata": undefined, }}); }); + test("errors case", ()=>{ + const errorResponse = { + "errors": { + "skyflowId" : "id", + "column": "col", + "error": "token not found" + } + + } + const fnResponse = formatForRenderClient(errorResponse, 'col'); + expect(fnResponse).toStrictEqual({ errors :{ + "skyflowId" : "id", + "column": "col", + "error": "token not found" + }}); + }) +}); describe('getFileURLFromVaultBySkyflowID', () => { it('should resolve with the file URL when the promise is resolved', async () => { const mockSkyflowIdRecord = { @@ -193,7 +228,7 @@ describe('getFileURLFromVaultBySkyflowID', () => { // console.log(mockClient.toJSON().metaData); const result = getFileURLFromVaultBySkyflowID(mockSkyflowIdRecord, mockClient); - console.log(result); + jest.spyOn(mockClient, 'request').mockResolvedValue('mockFileURL'); expect(result).toBeDefined(); }); @@ -205,11 +240,247 @@ describe('getFileURLFromVaultBySkyflowID', () => { }; const mockClient = Client.fromJSON(clientData.clientJSON); - // console.log(mockClient.toJSON().metaData); + jest.spyOn(mockClient, 'request').mockRejectedValue({error: { + code: '500', + description: 'Internal Server Error', + } + }); + + await expect(getFileURLFromVaultBySkyflowID(mockSkyflowIdRecord, mockClient)).rejects.toEqual({ + error: { + code: '500', + description: 'Internal Server Error', + }, + column: 'mockColumn', + skyflowId: 'mockSkyflowID', + }); + }); + it('should reject with an error when the promise is root rejected', async () => { + const mockSkyflowIdRecord = { + column: 'mockColumn', + skyflowID: 'mockSkyflowID', + table: 'mockTable', + }; + + const mockClient = Client.fromJSON(clientData.clientJSON); + jest.spyOn(mockClient, 'request').mockImplementation(() => { + throw { + error: { + code: '500', + description: 'Internal Server Error', + } + }; + }); - expect(getFileURLFromVaultBySkyflowID(mockSkyflowIdRecord, mockClient)).rejects.toThrow(); + await expect(getFileURLFromVaultBySkyflowID(mockSkyflowIdRecord, mockClient)).rejects.toEqual({ + error: { + code: '500', + description: 'Internal Server Error', + }, + }); }); }); + +describe('getFileURLFromVaultBySkyflowID for composable reveal', () => { + it('should resolve with the file URL when the promise is resolved', async () => { + const mockSkyflowIdRecord = { + column: 'mockColumn', + skyflowID: 'mockSkyflowID', + table: 'mockTable', + }; + + const mockClient = Client.fromJSON(clientData.clientJSON); + // mock getFileURLForRender + jest.spyOn(mockClient, 'request').mockResolvedValue('mockFileURL'); + + const result = getFileURLFromVaultBySkyflowIDComposable(mockSkyflowIdRecord, mockClient, "token"); + console.log('result',result); + expect(result).toBeDefined(); + }); + + it('should reject with an error when the promise is rejected', async () => { + const mockSkyflowIdRecord = { + column: 'mockColumn', + skyflowID: 'mockSkyflowID', + table: 'mockTable', + }; + + const mockClient = Client.fromJSON(clientData.clientJSON); + jest.spyOn(mockClient, 'request').mockRejectedValue({error: { + code: '500', + description: 'Internal Server Error', + } + }); + + await expect(getFileURLFromVaultBySkyflowIDComposable(mockSkyflowIdRecord, mockClient, "token")).rejects.toEqual({ + error: { + code: '500', + description: 'Internal Server Error', + }, + column: 'mockColumn', + skyflowId: 'mockSkyflowID', + }); + }); + it('should reject with an error when the promise is root rejected', async () => { + const mockSkyflowIdRecord = { + column: 'mockColumn', + skyflowID: 'mockSkyflowID', + table: 'mockTable', + }; + + const mockClient = Client.fromJSON(clientData.clientJSON); + jest.spyOn(mockClient, 'request').mockImplementation(() => { + throw { + error: { + code: '500', + description: 'Internal Server Error', + } + }; + }); + + await expect(getFileURLFromVaultBySkyflowIDComposable(mockSkyflowIdRecord, mockClient, "token")).rejects.toEqual({ + error: { + code: '500', + description: 'Internal Server Error', + }, + }); + }); +}); + +describe('fetchRecordsByTokenIdComposable', () => { + it('should resolve with records when all tokens are successfully detokenized', async () => { + const mockTokenRecords = [ + { token: 'token1', iframeName: 'iframe1', redaction: 'PLAIN_TEXT' }, + { token: 'token2', iframeName: 'iframe2', redaction: 'PLAIN_TEXT' }, + ]; + + const mockClient = Client.fromJSON(clientData.clientJSON); + + jest.spyOn(mockClient, 'request') + .mockResolvedValueOnce({ records: [{ token: 'token1', value: 'value1', valueType: 'STRING' }] }) + .mockResolvedValueOnce({ records: [{ token: 'token2', value: 'value2', valueType: 'STRING' }] }); + + const result = await fetchRecordsByTokenIdComposable(mockTokenRecords, mockClient, 'mockToken'); + + expect(result).toBeDefined(); + expect(result.records).toHaveLength(2); + expect(mockClient.request).toHaveBeenCalledTimes(2); + }); + + it('should reject with errors when all tokens fail to detokenize', async () => { + const mockTokenRecords = [ + { token: 'token1', iframeName: 'iframe1', redaction: 'PLAIN_TEXT' }, + ]; + + const mockClient = Client.fromJSON(clientData.clientJSON); + + jest.spyOn(mockClient, 'request').mockRejectedValue({ + error: { + code: '404', + description: 'Token not found', + }, + }); + + await expect(fetchRecordsByTokenIdComposable(mockTokenRecords, mockClient, 'mockToken')) + .rejects.toEqual({ + errors: expect.arrayContaining([ + expect.objectContaining({ + token: 'token1', + error: expect.any(Object), + }), + ]), + }); + }); + + it('should reject with both records and errors when some tokens succeed and some fail', async () => { + const mockTokenRecords = [ + { token: 'token1', iframeName: 'iframe1', redaction: 'PLAIN_TEXT' }, + { token: 'token2', iframeName: 'iframe2', redaction: 'PLAIN_TEXT' }, + ]; + + const mockClient = Client.fromJSON(clientData.clientJSON); + + jest.spyOn(mockClient, 'request') + .mockResolvedValueOnce({ records: [{ token: 'token1', value: 'value1', valueType: 'STRING' }] }) + .mockRejectedValueOnce({ + error: { + code: '404', + description: 'Token not found', + }, + }); + + await expect(fetchRecordsByTokenIdComposable(mockTokenRecords, mockClient, 'mockToken')) + .rejects.toEqual({ + records: expect.any(Array), + errors: expect.any(Array), + }); + }); + + it('should use default PLAIN_TEXT redaction when redaction is not provided', async () => { + const mockTokenRecords = [ + { token: 'token1', iframeName: 'iframe1' }, // No redaction + ]; + + const mockClient = Client.fromJSON(clientData.clientJSON); + + jest.spyOn(mockClient, 'request').mockResolvedValue({ + records: [{ token: 'token1', value: 'value1', valueType: 'STRING' }], + }); + + await fetchRecordsByTokenIdComposable(mockTokenRecords, mockClient, 'mockToken'); + + expect(mockClient.request).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.stringContaining('PLAIN_TEXT'), + }) + ); + }); + + it('should handle empty token records array', async () => { + const mockClient = Client.fromJSON(clientData.clientJSON); + + const result = await fetchRecordsByTokenIdComposable([], mockClient, 'mockToken'); + + expect(result).toEqual({ records: [] }); + }); + + it('should include frameId in the response', async () => { + const mockTokenRecords = [ + { token: 'token1', iframeName: 'iframe1', redaction: 'PLAIN_TEXT' }, + ]; + + const mockClient = Client.fromJSON(clientData.clientJSON); + + jest.spyOn(mockClient, 'request').mockResolvedValue({ + records: [{ token: 'token1', value: 'value1', valueType: 'STRING' }], + }); + + const result = await fetchRecordsByTokenIdComposable(mockTokenRecords, mockClient, 'mockToken'); + + expect(result.records[0]).toHaveProperty('frameId', 'iframe1'); + }); +}); +describe("formatRecordsForClient fn test",()=>{ + test("only success records",()=>{ + const testInput = {"records":[{"token":"7402-2242-2342-232","value":"231", "valueType" : "STRING"}] } + const fnResponse = formatRecordsForClient(testInput, {"7402-2242-2342-232": "231"}); + expect(fnResponse.success.length).toBe(1); + expect(fnResponse.errors).toBeUndefined(); + }); + test("both success and error records",()=>{ + const testInput = {"records":[{"token":"7402-2242-2342-232","value":"231", "valueType" : "STRING"}],"errors":[{"token":"3232-6434-3253-4221"}]}; + const fnResponse = formatRecordsForClient(testInput,{"7402-2242-2342-232": "231"}); + expect(fnResponse.errors.length).toBe(1); + expect(fnResponse.success.length).toBe(1); + }); + test("only error records",()=>{ + const testInput = {"errors":[{"token":"3232-6434-3253-4221"}]}; + const fnResponse = formatRecordsForClient(testInput); + expect(fnResponse.errors.length).toBe(1); + expect(fnResponse.success).toBeUndefined(); + }); +}); + describe('getFileURLForRender', () => { it('should return the file URL when the request is successful', async () => { const mockSkyflowIdRecord = { @@ -240,4 +511,3 @@ describe('getFileURLForRender', () => { expect(getFileURLForRender(mockSkyflowIdRecord, mockClient, 'mockAuthToken')).rejects.toThrow(); }); }); -}); diff --git a/tests/core/external/collect/collect-container.test.js b/tests/core/external/collect/collect-container.test.js index cbb0d666..6876f546 100644 --- a/tests/core/external/collect/collect-container.test.js +++ b/tests/core/external/collect/collect-container.test.js @@ -1618,7 +1618,7 @@ describe('Collect container', () => { expect(err).toBeDefined(); }) }); - it('test collect and additional fields duplicate elements',(done)=>{ + it('test collect and additional fields duplicate elements',()=>{ const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -1632,11 +1632,12 @@ describe('Collect container', () => { done(res) }).catch((err)=>{ expect(err).toBeDefined(); - done(); + // done(); }); + }catch(err){ expect(err).toBeDefined(); - done(err) + // done() } }); diff --git a/tests/core/external/collect/collect-container.test.ts b/tests/core/external/collect/collect-container.test.ts index fd778506..fbbf0954 100644 --- a/tests/core/external/collect/collect-container.test.ts +++ b/tests/core/external/collect/collect-container.test.ts @@ -62,6 +62,7 @@ const metaData: Metadata = { skyflowContainer: { isControllerFrameReady: true, } as unknown as SkyflowContainer, + getSkyflowBearerToken: getBearerToken, }; const metaData2: Metadata = { diff --git a/tests/core/external/collect/composable-container.test.js b/tests/core/external/collect/composable-container.test.js index c77561f2..3229ced1 100644 --- a/tests/core/external/collect/composable-container.test.js +++ b/tests/core/external/collect/composable-container.test.js @@ -1,6 +1,7 @@ import { COLLECT_FRAME_CONTROLLER, - ELEMENT_EVENTS_TO_IFRAME + ELEMENT_EVENTS_TO_IFRAME, + ElementType } from '../../../../src/core/constants'; import * as iframerUtils from '../../../../src/iframe-libs/iframer'; import { LogLevel, Env, ValidationRuleType } from '../../../../src/utils/common'; @@ -18,7 +19,7 @@ const bus = require('framebus'); iframerUtils.getIframeSrc = jest.fn(() => ('https://google.com')); -const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve('token')); const mockUuid = '1234'; jest.mock('../../../../src/libs/uuid', () => ({ @@ -57,6 +58,7 @@ EventEmitter.mockImplementation(()=>({ const metaData = { + getSkyflowBearerToken: getBearerToken, skyflowContainer:{ isControllerFrameReady: true }, @@ -78,6 +80,7 @@ const metaData = { }, }; const metaData2 = { + getSkyflowBearerToken: getBearerToken, skyflowContainer:{ isControllerFrameReady: false }, @@ -137,9 +140,16 @@ const cardNumberElement = { column: 'primary_card.card_number', type: 'CARD_NUMBER', ...collectStylesOptions, - }; +const FileInuptElement = { + table: 'pii_fields', + column: 'profile_picture', + type: ElementType.FILE_INPUT, + skyflowID:'id1', + ...collectStylesOptions, +} + const ExpirationDateElement = { table: 'pii_fields', column: 'primary_card.expiry', @@ -174,6 +184,7 @@ describe('test composable container class',()=>{ let targetSpy; let onSpy; let eventEmitterSpy; + let windowSpy; beforeEach(() => { emitSpy = jest.spyOn(bus, 'emit'); targetSpy = jest.spyOn(bus, 'target'); @@ -183,6 +194,7 @@ describe('test composable container class',()=>{ on, off: jest.fn() }); + windowSpy = jest.spyOn(window, "window", "get"); }); @@ -240,10 +252,6 @@ describe('test composable container class',()=>{ }); it('test collect with success and error scenarios', async () => { - // let readyCb; - // on.mockImplementation((name, cb) => { - // readyCb = cb; - // }); const div = document.createElement('div'); div.id = 'composable'; @@ -284,19 +292,29 @@ describe('test composable container class',()=>{ ], }; - const collectPromiseSuccess = container.collect(options); - - const collectCb1 = emitSpy.mock.calls[0][2]; - collectCb1(collectResponse); - + const collectPromiseSuccess = + container.collect(options); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + '1234', // containerId + data: {...collectResponse} + } + })); + const successResult = await collectPromiseSuccess; expect(successResult).toEqual(collectResponse); - - const collectPromiseError = container.collect(options); - const collectCb2 = emitSpy.mock.calls[1][2]; - collectCb2({ error: 'Error occurred' }); - - await expect(collectPromiseError).rejects.toEqual('Error occurred'); + + const collectPromiseError = + container.collect(options); + + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + '1234', // containerId + data: { error: "Error occured"} + } + })); + await expect(collectPromiseError).rejects.toEqual("Error occured"); + }); it('test collect when isMount is false', async () => { let readyCb; @@ -544,5 +562,80 @@ describe('test composable container class',()=>{ container.on("CHANGE",()=>{}); expect(element).toBeInstanceOf(ComposableElement); }); + it('test upload FILES 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: [1], styles: { base: { width: '100px' } } } + ); + + const element1 = container.create(FileInuptElement); + container.mount('#composable'); + const options = {}; + + const collectPromiseSuccess = container.uploadFiles(options); + + // Wait for the bearer token promise to resolve and event listener to be set up + await Promise.resolve('token'); + + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + '1234', // containerId + data: { fileUploadResponse: [{ skyflow_id: 'id1' }] } + } + })); + + const successResult = await collectPromiseSuccess; + expect(successResult).toEqual({ fileUploadResponse: [{ skyflow_id: 'id1' }] }); + // Test error scenario + const collectPromiseError = container.uploadFiles(options); + + await Promise.resolve('token'); + + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + '1234', // containerId + data: { error: "Error occured"} + } + })); + + await expect(collectPromiseError).rejects.toEqual("Error occured"); + + // Test error scenario case 2 - no fileUploadResponse and no error + const collectPromiseError2 = container.uploadFiles(options); + + await Promise.resolve('token'); + + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + '1234', // containerId + data: { errors: "Error occured"} + } + })); + + await expect(collectPromiseError2).rejects.toEqual({ errors: "Error occured"}); + }); + it('test upload FILES when bearer token fails', async () => { + const getBearerTokenFail = jest.fn().mockRejectedValue({ error: 'token generation failed' }); + const metaDataFail = { + ...metaData, + getSkyflowBearerToken: getBearerTokenFail, + }; + + const div = document.createElement('div'); + div.id = 'composable2'; + document.body.append(div); + + const container = new ComposableContainer(metaDataFail, {}, context, { layout: [1] }); + const element1 = container.create(FileInuptElement); + container.mount('#composable2'); + + await expect(container.uploadFiles({})).rejects.toEqual({ error: 'token generation failed' }); + }); }); \ No newline at end of file diff --git a/tests/core/external/collect/composable-container.test.ts b/tests/core/external/collect/composable-container.test.ts index 94eee38d..f2af6f76 100644 --- a/tests/core/external/collect/composable-container.test.ts +++ b/tests/core/external/collect/composable-container.test.ts @@ -24,6 +24,13 @@ 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"; +import IFrame from "../../../../src/core/external/common/iframe"; + +global.ResizeObserver = jest.fn(() => ({ + observe: jest.fn(), + disconnect: jest.fn(), + unobserve: jest.fn(), +})); const bus = require("framebus"); @@ -37,7 +44,7 @@ jest.mock("../../../../src/iframe-libs/iframer", () => { return mockedModule; }); -const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve("token")); const mockUuid = "1234"; jest.mock("../../../../src/libs/uuid", () => ({ @@ -96,6 +103,7 @@ const metaData: Metadata = { clientDomain: "http://abc.com", }, }, + getSkyflowBearerToken: getBearerToken, skyflowContainer: { isControllerFrameReady: true, } as unknown as SkyflowContainer, @@ -257,7 +265,6 @@ describe("test composable container class", () => { const div = document.createElement("div"); div.id = "composable"; document.body.append(div); - const container = new ComposableContainer(metaData, [], context, { layout: [2], styles: { base: { width: "100px" } }, @@ -267,7 +274,6 @@ describe("test composable container class", () => { const element2 = container.create(cardNumberElement); container.mount("#composable"); - const options: ICollectOptions = { tokens: true, additionalFields: { @@ -290,19 +296,26 @@ describe("test composable container class", () => { const collectPromiseSuccess: Promise = container.collect(options); - - const collectCb1 = emitSpy.mock.calls[0][2]; - collectCb1(collectResponse); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + '1234', // containerId + data: {...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"); + + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + '1234', // containerId + data: { error: "Error occured"} + } + })); + await expect(collectPromiseError).rejects.toEqual("Error occured"); }); it("tests collect when isMount is false", async () => { diff --git a/tests/core/external/reveal/reveal-composable-container.test.js b/tests/core/external/reveal/reveal-composable-container.test.js new file mode 100644 index 00000000..76ca5333 --- /dev/null +++ b/tests/core/external/reveal/reveal-composable-container.test.js @@ -0,0 +1,503 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import RevealContainer from "../../../../src/core/external/reveal/reveal-container"; +import { ComposableRevealContainer, ComposableRevealElement } from "../../../../src/index-node"; +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 * as iframerUtils from '../../../../src/iframe-libs/iframer'; +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"; + +iframerUtils.getIframeSrc = jest.fn(() => ('https://google.com')); +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); +describe("Reveal Composable Container Class", () => { + let emitSpy; + let targetSpy; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, 'emit'); + targetSpy = jest.spyOn(bus, 'target'); + targetSpy.mockReturnValue({ + on, + off + }); + }); + const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve('token')); + const testMetaData = { + getSkyflowBearerToken: getBearerToken, + skyflowContainer: { + isControllerFrameReady: true, + }, + uuid: "123", + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev.com", + getBearerToken, + }, + metaData: { + clientDomain: "http://abc.com", + }, + }; + const testRevealContainer = new ComposableRevealContainer(testMetaData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] +}); + const skyflowConfig = { + vaultID: 'e20afc3ae1b54f0199f24130e51e0c11', + vaultURL: 'https://testurl.com', + getBearerToken, + }; + + const clientData = { + getSkyflowBearerToken: getBearerToken, + skyflowContainer: { + isControllerFrameReady: false, + }, + uuid: '1234', + client: { + config: { ...skyflowConfig }, + metaData: { + uuid: "1234", + }, + }, + clientJSON: { + config: { + vaultID: 'vault123', + vaultURL: 'https://sb.vault.dev', + getBearerToken, + }, + }, + } + + const clientData2 = { + skyflowContainer: { + isControllerFrameReady: true, + }, + uuid: '1234', + client: { + config: { ...skyflowConfig }, + metaData: { + uuid: "1234", + }, + }, + clientJSON:{ + context: { logLevel: LogLevel.ERROR,env:Env.PROD}, + metaData: { + uuid: "1234", + }, + config:{ + ...skyflowConfig, + getBearerToken + } + } + } + + const testRecord = { + token: "1677f7bd-c087-4645-b7da-80a6fd1a81a4", + // redaction: RedactionType.PLAIN_TEXT, + label: "", + styles: { + base: { + color: "#32ce21", + }, + }, + }; + test("reveal should throw error with no elements", (done) => { + const container = new ComposableRevealContainer(clientData, [], { 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_COMPOSABLE); + }) + }); + + test("constructor", () => { + expect(testRevealContainer).toBeInstanceOf(ComposableRevealContainer); + expect(testRevealContainer).toBeInstanceOf(Object); + expect(testRevealContainer).toHaveProperty("create"); + expect(testRevealContainer).toHaveProperty("reveal"); + expect(testRevealContainer).toHaveProperty("type"); + }); + test("create() will return a Reveal composable element", () => { + const testRevealElement = testRevealContainer.create(testRecord); + expect(testRevealElement).toBeInstanceOf(ComposableRevealElement); + }); + test("create() will throw error if record id invalid", () => { + try { + testRevealContainer.create({ + token: "", + // redaction: RedactionType.REDACTED, + }); + } catch (error) { + expect(error.message).toBe("Invalid Token Id "); + } + try { + testRevealContainer.create({ + token: true, + // redaction: RedactionType.PLAIN_TEXT, + }); + } catch (error) { + expect(error.message).toBe("Invalid Token Id true"); + } + }); + + test('create() will throw error for invalid input format options',(done)=>{ + try { + testRevealContainer.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("on container mounted call back",()=>{ + const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] + }); + testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const data = { + token: "1815-6223-1073-1425", + containerId:mockUuid + } + const div = document.createElement('div'); + div.id = 'container'; + document.body.appendChild(div); + testRevealContainer.mount('#container'); + }); +// test("on container mounted call back 5",()=>{ +// const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); +// testRevealContainer.create({ +// token: "token", +// }); +// const data = { +// token: "1815-6223-1073-1425", +// containerId:mockUuid +// } +// const eventName = ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED+mockUuid +// bus.emit(eventName,data); +// const onCbName = on.mock.calls[0][0]; +// expect(onCbName).toBe(eventName); +// const onCb = on.mock.calls[0][1]; +// onCb(data); +// testRevealContainer.reveal(); +// const frameEventName = ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid +// const onframeEvent = on.mock.calls[1][0]; +// expect(frameEventName).toBe(onframeEvent); +// const onCbFrame = on.mock.calls[1][1]; +// onCbFrame({}); +// const emitEventName = emitSpy.mock.calls[1][0]; +// const emitCb = emitSpy.mock.calls[1][2]; +// expect(emitEventName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS+mockUuid); +// emitCb({"success":[{token:"1815-6223-1073-1425"}]}); +// }); + +// test("on container mounted else call back",()=>{ +// const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); +// testRevealContainer.create({ +// token: "1815-6223-1073-1425", +// }); +// const data = { +// token: "1815-6223-1073-1425", +// containerId:mockUuid +// } + +// testRevealContainer.reveal().catch(err => { +// console.log(err); +// }); +// const eventName = ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED+mockUuid +// bus.emit(eventName,data); +// const onCbName = on.mock.calls[0][0]; +// expect(onCbName).toBe(eventName); +// const onCb = on.mock.calls[0][1]; +// onCb(data); + +// const frameEventName = ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid +// const onframeEvent = on.mock.calls[1][0]; +// expect(frameEventName).toBe(onframeEvent); +// const onCbFrame = on.mock.calls[1][1]; +// onCbFrame({}); + +// const emitEventName = emitSpy.mock.calls[1][0]; +// const emitCb = emitSpy.mock.calls[1][2]; +// expect(emitEventName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS+mockUuid); +// emitCb({error:{code:404,description:"Not Found"}}); +// }); +// test("on container mounted else call back 1",()=>{ +// const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); +// testRevealContainer.create({ +// token: "1815-6223-1073-1425", +// }); +// const data = { +// token: "1815-6223-1073-1425", +// containerId:mockUuid +// } + + +// testRevealContainer.reveal(); +// const eventName = ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED+mockUuid +// bus.emit(eventName,data); + +// const onCbName = on.mock.calls[0][0]; +// expect(onCbName).toBe(eventName); +// const onCb = on.mock.calls[0][1]; +// onCb(data); +// const frameEventName = ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid +// const onframeEvent = on.mock.calls[1][0]; +// expect(frameEventName).toBe(onframeEvent); +// const onCbFrame = on.mock.calls[1][1]; +// onCbFrame({}); + +// const emitEventName = emitSpy.mock.calls[1][0]; +// const emitCb = emitSpy.mock.calls[1][2]; +// expect(emitEventName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS+mockUuid); +// emitCb({"success":[{token:"1815-6223-1073-1425"}]}); +// }); + test("reveal before skyflow frame ready event",async ()=>{ + const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] + }); + testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const data = { + token: "1815-6223-1073-1425", + containerId:mockUuid + } + + const res = testRevealContainer.reveal(); + await Promise.resolve('token'); + expect(res).toBeInstanceOf(Promise); //ELEMENT_EVENTS_TO_CLIENT.MOUNTED + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid, + data: data + } + })); + await Promise.resolve(); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + mockUuid, + data: {"success":[{token:"1815-6223-1073-1425"}]} + } + })); + + await expect(res).resolves.toEqual({"success":[{token:"1815-6223-1073-1425"}]}); + }); + test("reveal before skyflow frame ready event, Error case",async ()=>{ + const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] + }); + testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const data = { + token: "1815-6223-1073-1425", + containerId:mockUuid + } + + const res = testRevealContainer.reveal(); + await Promise.resolve('token'); + expect(res).toBeInstanceOf(Promise); //ELEMENT_EVENTS_TO_CLIENT.MOUNTED + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid, + data: data + } + })); + await Promise.resolve(); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + mockUuid, + data: {"errors":{ + code:404, + description:"Not Found" + }} + } + })); + + await expect(res).rejects.toEqual({"errors":{code:404,description:"Not Found"}}); + }); + test("reveal before skyflow frame ready event, Error case when bearer token step failed",async ()=>{ + // Create a mock that rejects for bearer token + const getBearerTokenFail = jest.fn().mockRejectedValue({ + errors: { + code: 400, + description: "Failed to fetch bearer token" + } + }); + + const clientDataFail = { + ...clientData, + getSkyflowBearerToken: getBearerTokenFail, + }; + + const testRevealContainer = new ComposableRevealContainer(clientDataFail, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] + }); + testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const data = { + token: "1815-6223-1073-1425", + containerId:mockUuid + } + + const res = testRevealContainer.reveal(); + + await expect(res).rejects.toEqual({errors:{code:400,description:"Failed to fetch bearer token"}}); + }); + + /// frame ready event + test("reveal before skyflow frame ready event",async ()=>{ + const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] + }); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid, + data: data + } + })); + testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const data = { + token: "1815-6223-1073-1425", + containerId:mockUuid + } + + const res = testRevealContainer.reveal(); + await Promise.resolve('token'); + expect(res).toBeInstanceOf(Promise); //ELEMENT_EVENTS_TO_CLIENT.MOUNTED + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + mockUuid, + data: {"success":[{token:"1815-6223-1073-1425"}]} + } + })); + + await expect(res).resolves.toEqual({"success":[{token:"1815-6223-1073-1425"}]}); + }); + test("reveal before skyflow frame ready event, Error case",async ()=>{ + const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] + }); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid, + data: data + } + })); + testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const data = { + token: "1815-6223-1073-1425", + containerId:mockUuid + } + + const res = testRevealContainer.reveal(); + await Promise.resolve('token'); + expect(res).toBeInstanceOf(Promise); //ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + mockUuid, + data: {"errors":{ + code:404, + description:"Not Found" + }} + } + })); + + await expect(res).rejects.toEqual({"errors":{code:404,description:"Not Found"}}); + }); + test("reveal before skyflow frame ready event, Error case when bearer token step failed",async ()=>{ + // Create a mock that rejects for bearer token + const getBearerTokenFail = jest.fn().mockRejectedValue({ + errors: { + code: 400, + description: "Failed to fetch bearer token" + } + }); + + const clientDataFail = { + ...clientData, + getSkyflowBearerToken: getBearerTokenFail, + }; + + const testRevealContainer = new ComposableRevealContainer(clientDataFail, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] + }); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid, + data: data + } + })); + testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const data = { + token: "1815-6223-1073-1425", + containerId:mockUuid + } + + const res = testRevealContainer.reveal(); + + await expect(res).rejects.toEqual({errors:{code:400,description:"Failed to fetch bearer token"}}); + }); + + test("reveal when elment is empty when skyflow ready",(done)=>{ + const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] + }); + + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid, + data: 'data' + } + })); + testRevealContainer.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_COMPOSABLE); + }) + }); + test("reveal when elment is empty when skyflow frame not ready",(done)=>{ + const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + layout:[1] + }); + testRevealContainer.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_COMPOSABLE); + }) + }); +}); diff --git a/tests/core/external/reveal/reveal-composable-element.test.js b/tests/core/external/reveal/reveal-composable-element.test.js new file mode 100644 index 00000000..47a7df73 --- /dev/null +++ b/tests/core/external/reveal/reveal-composable-element.test.js @@ -0,0 +1,222 @@ +/* +Copyright (c) 2022 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 SkyflowContainer from '../../../../src/core/external/skyflow-container'; +import Client from '../../../../src/client'; +import EventEmitter from "../../../../src/event-emitter"; +import * as busEvents from '../../../../src/utils/bus-events'; +import ComposableRevealInternalElement from "../../../../src/core/external/reveal/composable-reveal-internal"; + +import bus from "framebus"; +import { JSDOM } from 'jsdom'; +import { ComposableRevealElement, EventName, RedactionType } from "../../../../src/index-node"; + +busEvents.getAccessToken = jest.fn(() => Promise.reject('access token')); + +const mockUuid = '1234'; +jest.mock('../../../../src/libs/uuid',()=>({ + __esModule: true, + default:jest.fn(()=>(mockUuid)), +})); +// const _on = jest.fn(); +// const _off = jest.fn(); +// const _emit = jest.fn(); +const getBearerToken = jest.fn(); + +const groupEmittFn = jest.fn(); +let groupOnCb; +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(), + } +}) + +// jest.mock('../../../../src/core/external/reveal/composable-reveal-internal') + +// bus.on = _on; +// bus.target = jest.fn().mockReturnValue({ +// on: _on, +// }); +// bus.off = _off; +// bus.emit = _emit; + +const clientDomain = "http://abc.com"; +const skyflowConfig = { + vaultID: 'e20afc3ae1b54f0199f24130e51e0c11', + vaultURL: 'https://testurl.com', + getBearerToken: jest.fn(), + options: { trackMetrics: true, trackingKey: "key" } +}; +let controller = new SkyflowContainer(client,{ + logLevel:LogLevel.DEBUG, + env:Env.DEV +}); + +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 client = new Client(clientData.client.config, clientData); + +const on = jest.fn(); +const off = jest.fn(); +let skyflowContainer; +describe("Reveal Composable Element Class", () => { + let emitSpy; + let targetSpy; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, 'emit'); + targetSpy = jest.spyOn(bus, 'target'); + targetSpy.mockReturnValue({ + on, + off + }); + const client = new Client(clientData.client.config, clientData); + skyflowContainer = new SkyflowContainer(client, { logLevel: LogLevel.DEBUG, env: Env.PROD }); + }); + + test("constructor", () => { + const eventEmitter = new EventEmitter(); + const testRevealElement = new ComposableRevealElement( + "name", + eventEmitter, + '123', + ); + eventEmitter._emit(`${EventName.READY}:name`, {}); + expect(testRevealElement).toBeInstanceOf(ComposableRevealElement); + + }); + test("iframe name", () => { + const testRevealElement = new ComposableRevealElement( + "name", + new EventEmitter(), + '123', + ); + expect(testRevealElement.iframeName()).toBe('123'); + }); + test("getID", () => { + const testRevealElement = new ComposableRevealElement( + "name", + new EventEmitter(), + '123', + ); + expect(testRevealElement.getID()).toBe('name'); + }); + test("file render call success case", async () => { + const eventEmitter = new EventEmitter(); + const testRevealElement = new ComposableRevealElement( + "name", + eventEmitter, + '123', + ); + eventEmitter.on(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ':name', (data, cb) => { + console.log('data', data); + cb({ success: { skyflow_id: '1244', column: 'column' } }); + }); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + + const res = testRevealElement.renderFile() + await expect(res).resolves.toEqual({ success: { skyflow_id: '1244', column: 'column' } }); + }); + test("file render call error case 1", async () => { + const eventEmitter = new EventEmitter(); + const testRevealElement = new ComposableRevealElement( + "name", + eventEmitter, + '123', + ); + eventEmitter.on(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ':name', (data, cb) => { + console.log('data', data); + cb({ errors: { skyflow_id: '1244', column: 'column', error:{ + code: 400, description: "No Records Found" + } } }); + }); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + + const res = testRevealElement.renderFile() + await expect(res).rejects.toEqual({"errors": {"column": "column", "error": {"code": 400, "description": "No Records Found"}, "skyflow_id": "1244"}}); + }); + test("file render call error case 2", async () => { + const eventEmitter = new EventEmitter(); + const testRevealElement = new ComposableRevealElement( + "name", + eventEmitter, + '123', + ); + eventEmitter.on(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ':name', (data, cb) => { + console.log('data', data); + cb({error: + { + code: 400, + description: "No Records Found" + } + }); + }); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + const res = testRevealElement.renderFile() + await expect(res).rejects.toEqual({"errors": {"code": 400, "description": "No Records Found"}}); + }); + test("update method", async () => { + const eventEmitter = new EventEmitter(); + const testRevealElement = new ComposableRevealElement( + "name", + eventEmitter, + '123', + ); + eventEmitter.on(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + ':name', (data, cb) => { + expect(data).toEqual({ + options: {redaction: RedactionType.MASKED}, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS + }); + }); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + testRevealElement.update({redaction: RedactionType.MASKED}) + }); +}); diff --git a/tests/core/external/reveal/reveal-composable-internal.test.js b/tests/core/external/reveal/reveal-composable-internal.test.js new file mode 100644 index 00000000..a7407759 --- /dev/null +++ b/tests/core/external/reveal/reveal-composable-internal.test.js @@ -0,0 +1,787 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import { LogLevel,Env } from "../../../../src/utils/common"; +import { ELEMENT_EVENTS_TO_IFRAME, COMPOSABLE_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 ComposableRevealInternalElement from "../../../../src/core/external/reveal/composable-reveal-internal"; +import * as busEvents from '../../../../src/utils/bus-events'; +import bus from "framebus"; +import { JSDOM } from 'jsdom'; +import EventEmitter from "../../../../src/event-emitter"; +import { error } from "console"; + +busEvents.getAccessToken = jest.fn(() => Promise.reject('access token')); + +const mockUuid = '1234'; +const elementId = 'id'; +jest.mock('../../../../src/libs/uuid',()=>({ + __esModule: true, + default:jest.fn(()=>(mockUuid)), +})); +global.ResizeObserver = jest.fn(() => ({ + observe: jest.fn(), + disconnect: jest.fn(), + unobserve: jest.fn(), +})); +// const _on = jest.fn(); +// const _off = jest.fn(); +// const _emit = jest.fn(); +const getBearerToken = jest.fn().mockResolvedValue('token'); + +const groupEmittFn = jest.fn(); +let groupOnCb; +const groupEmiitter = { + _emit: groupEmittFn, + on:jest.fn().mockImplementation((args,cb)=>{ + groupOnCb = cb; + }) +} +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(), + } +}) + +// bus.on = _on; +// bus.target = jest.fn().mockReturnValue({ +// on: _on, +// }); +// bus.off = _off; +// bus.emit = _emit; + +const clientDomain = "http://abc.com"; +const metaData = { + uuid: "123", + skyflowContainer: { + isControllerFrameReady: true, + }, + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev", + getAccessToken: getBearerToken, + }, + metaData: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev" + }, + clientDomain: clientDomain, + getSkyflowBearerToken: getBearerToken, + clientJSON:{ + context: { logLevel: LogLevel.ERROR,env:Env.PROD}, + config:{ + ...skyflowConfig, + getBearerToken:jest.fn().toString() + } + }, +}; +const skyflowConfig = { + vaultID: 'e20afc3ae1b54f0199f24130e51e0c11', + vaultURL: 'https://testurl.com', + getBearerToken: jest.fn(), + options: { trackMetrics: true, trackingKey: "key" } +}; +let controller = new SkyflowContainer(client,{ + logLevel:LogLevel.DEBUG, + env:Env.DEV +}); + +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, + getSkyflowBearerToken: getBearerToken, +} +const client = new Client(clientData?.client?.config, clientData); + +const on = jest.fn(); +const off = jest.fn(); +let skyflowContainer; +describe("Reveal Element Class", () => { + let emitSpy; + let targetSpy; + let windowSpy; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, 'emit'); + targetSpy = jest.spyOn(bus, 'target'); + targetSpy.mockReturnValue({ + on, + off + }); + windowSpy = jest.spyOn(window, "window", "get"); + const client = new Client(clientData.client.config, clientData); + skyflowContainer = new SkyflowContainer(client, { logLevel: LogLevel.DEBUG, env: Env.PROD }); + }); + afterEach(() => { + windowSpy.mockRestore(); + }); + + const containerId = mockUuid; + test("constructor", () => { + const testRevealElement = new ComposableRevealInternalElement( + elementId, + [], + metaData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { logLevel: LogLevel.ERROR,env:Env.PROD } + ); + expect(testRevealElement).toBeInstanceOf(ComposableRevealInternalElement); + }); + test("Mount Method", () => { + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element1" + }] + }] + }; + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + metaData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { logLevel: LogLevel.ERROR,env:Env.PROD } + ); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element1", + containerId: mockUuid, + } + })); + 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + }); + test("file render call success case", async () => { + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element1", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { logLevel: LogLevel.ERROR,env:Env.PROD } + ); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element1", + containerId: mockUuid, + } + })); + 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + // Mock iframe postMessage to prevent CI errors + const iframe = document.querySelector("iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ":element1", {}, (response) => { + expect(response).toBeDefined(); + expect(response).toEqual({ success: { skyflow_id: '1244', column: 'file' } }); + }); + await Promise.resolve('token'); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + "element1", + containerId: mockUuid, + data: { + result: { success: { skyflow_id: '1244', column: 'file' } }, + type: REVEAL_TYPES.RENDER_FILE, + } + } + })); + }); + test("file render call error case", async () => { + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element1", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { logLevel: LogLevel.ERROR,env:Env.PROD } + ); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element1", + containerId: mockUuid, + } + })); + 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + // Mock iframe postMessage to prevent CI errors + const iframe = document.querySelector("iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ":element1", {}, (response) => { + expect(response).toBeDefined(); + expect(response).toEqual({ error: { skyflow_id: '1244', column: 'file', error:{code : 400, description: "No Records Found"} } }); + }); + await Promise.resolve('token'); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + "element1", + containerId: mockUuid, + data: { + result: { errors: { skyflow_id: '1244', column: 'file', error:{ + code: 400, + description: "No Records Found", + } } }, + type: REVEAL_TYPES.RENDER_FILE, + } + } + })); + }); + test("file render call error case", async () => { + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element1", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { logLevel: LogLevel.ERROR,env:Env.PROD } + ); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element1", + containerId: mockUuid, + } + })); + 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + // Mock iframe postMessage to prevent CI errors + const iframe = document.querySelector("iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ":element1", {}, (response) => { + expect(response).toBeDefined(); + expect(response).toEqual({ error: { skyflow_id: '1244', column: 'file', error:{code : 400, description: "No Records Found"} } }); + }); + await Promise.resolve('token'); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + "element1", + containerId: mockUuid, + data: { + result: { errors: { skyflow_id: '1244', column: 'file', error:{ + code: 400, + description: "No Records Found", + } } }, + type: REVEAL_TYPES.RENDER_FILE, + } + } + })); + }); + test("file render call success case when mount event not happened ", async () => { + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element2", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + // Mock iframe postMessage to prevent CI errors + const iframe = document.querySelector("iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ":element2", {}, (response) => { + expect(response).toBeDefined(); + expect(response).toEqual({ success: { skyflow_id: '1244', column: 'file' } }); + }); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element2", + containerId: mockUuid, + } + })); + await Promise.resolve('token'); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + "element2", + containerId: mockUuid, + data: { + result: { success: { skyflow_id: '1244', column: 'file' } }, + type: REVEAL_TYPES.RENDER_FILE, + } + } + })); + }); + test("file render call error case when mount event not happened ", async () => { + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element2", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + // Mock iframe postMessage to prevent CI errors + const iframe = document.querySelector("iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ":element2", {}, (response) => { + expect(response).toBeDefined(); + expect(response).toEqual({ error: { skyflow_id: '1244', column: 'file', error:{code : 400, description: "No Records Found"} } }); + }); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element2", + containerId: mockUuid, + } + })); + await Promise.resolve('token'); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + "element2", + containerId: mockUuid, + data: { + result: { errors: { skyflow_id: '1244', column: 'file', error:{code : 400, description: "No Records Found"} } }, + type: REVEAL_TYPES.RENDER_FILE, + } + } + })); + }); + test("file render call error case when mount event not happened when bearer token call rejected", async () => { + // Mock the bearer token to reject + const getBearerTokenFail = jest.fn().mockRejectedValue({ + error: {code: 400, description: "BEARER TOKEN FAILED"} + }); + + const clientDataFail = { + ...clientData, + getSkyflowBearerToken: getBearerTokenFail, + }; + + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element2", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientDataFail, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + // Mock iframe postMessage to prevent CI errors + const iframe = document.querySelector("iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + // Capture the callback response + let callbackResponse; + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ":element2", {}, (response) => { + callbackResponse = response; + }); + + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element2", + containerId: mockUuid, + } + })); + + // Wait for the async bearer token call to complete + await new Promise(resolve => setTimeout(resolve, 50)); + + // Assert the callback was called with the error + expect(callbackResponse).toBeDefined(); + expect(callbackResponse).toEqual({error: {error: {code: 400, description: "BEARER TOKEN FAILED"}}}); + }); + test("file render call error case when bearer token call rejected", async () => { + // Mock the bearer token to reject + const getBearerTokenFail = jest.fn().mockRejectedValue({ + error: {code: 400, description: "BEARER TOKEN FAILED"} + }); + + const clientDataFail = { + ...clientData, + getSkyflowBearerToken: getBearerTokenFail, + }; + + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element2", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientDataFail, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { logLevel: LogLevel.ERROR,env:Env.PROD } + ); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element2", + containerId: mockUuid, + } + })); + 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + // Mock iframe postMessage to prevent CI errors + const iframe = document.querySelector("iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + // Capture the callback response + let callbackResponse; + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST + ":element2", {}, (response) => { + callbackResponse = response; + }); + + + // Wait for the async bearer token call to complete + await new Promise(resolve => setTimeout(resolve, 50)); + + // Assert the callback was called with the error + expect(callbackResponse).toBeDefined(); + expect(callbackResponse).toEqual({error: {error: {code: 400, description: "BEARER TOKEN FAILED"}}}); + }); + test("update call error case when container is mounted", async () => { + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element2", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + // Mock iframe postMessage to prevent CI errors + const iframe = document.querySelector("iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + ":element2", { + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + options: { altText: "Updated Alt Text" } + }); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element2", + containerId: mockUuid, + } + })); + }); + test("update call error case when container is not mounted", async () => { + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element2", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { logLevel: LogLevel.ERROR,env:Env.PROD } + ); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element2", + containerId: mockUuid, + } + })); + 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 = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + // Mock iframe postMessage to prevent CI errors + const iframe = document.querySelector("iframe"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + ":element2", { + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + options: { altText: "Updated Alt Text" } + }); + + + }); + test("update call error case when container is not mounted case 2", async () => { + const elementArray = { + rows:[{ + elements : [{ + "column": "file", + "table": "table6", + "altText": "Alt text 1", + "name": "element2", + "skyflowID": "id1" + }] + }] + }; + const groupEmiitter = new EventEmitter(); + const testRevealElement = new ComposableRevealInternalElement( + elementId, + elementArray, + clientData, + {containerId:containerId,isMounted:false,eventEmitter:groupEmiitter}, + { logLevel: LogLevel.ERROR,env:Env.PROD } + ); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + "element2", + containerId: mockUuid, + } + })); + + + // create shadow dom and add testDiv inside shadow dom + const hostElement = document.createElement('div'); + document.body.appendChild(hostElement); + hostElement.attachShadow({ mode: 'open' }); + + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + hostElement.shadowRoot.appendChild(testEmptyDiv); + expect(hostElement.shadowRoot.getElementById("testDiv")).not.toBeNull(); + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount(hostElement.shadowRoot.getElementById('testDiv')); + + // Mock iframe postMessage to prevent CI errors + const iframe = hostElement.shadowRoot.getElementById('testDiv').querySelector('iframe'); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.postMessage = jest.fn(); + } + + const divInShadow = hostElement.shadowRoot.getElementById('testDiv').querySelector('iframe'); + expect(divInShadow).not.toBeNull(); + + expect(hostElement.shadowRoot.getElementById('testDiv').querySelector('iframe')).toBeTruthy(); + const testIframeName = `${COMPOSABLE_REVEAL}:${btoa(mockUuid)}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(hostElement.shadowRoot.getElementById('testDiv').querySelector('iframe')?.name).toBe(testIframeName); + + groupEmiitter._emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + ":element2", { + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + options: { altText: "Updated Alt Text" } + }); + }); +}); \ No newline at end of file diff --git a/tests/core/external/reveal/reveal-container.test.js b/tests/core/external/reveal/reveal-container.test.js index 0b515317..146c916b 100644 --- a/tests/core/external/reveal/reveal-container.test.js +++ b/tests/core/external/reveal/reveal-container.test.js @@ -166,47 +166,6 @@ describe("Reveal Container Class", () => { done(); } }); - - // test.only("on skyflow frame ready call back",()=>{ - // const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); - // const data = { - // name:REVEAL_FRAME_CONTROLLER, - // }; - // const emitterCb = jest.fn(); - // console.log('data is on.mock.calls', on.mock.calls); - // const eventName = ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY+mockUuid - // const onCbName = on.mock.calls[1][0]; - // expect(onCbName).toBe(eventName); - // const onCb = on.mock.calls[1][1]; - // onCb(data, emitterCb); - // bus.emit(eventName,data,emitterCb); - // const data1 = { - // "client": { - // "config": { - // "vaultID": "e20afc3ae1b54f0199f24130e51e0c11", - // "vaultURL": "https://testurl.com", - // "getBearerToken": getBearerToken, - // "options": {} - // }, - - // "metaData": { - // "uuid": "1234", - // }, - // "context": { - // "env": "PROD", - // "logLevel": "ERROR" - // }, - // }, - // "context": { - // "env": "PROD", - // "logLevel": "ERROR" - // }, - // } - - // expect(emitterCb).toBeCalledTimes(1); - // expect(emitterCb).toBeCalledWith(data1); - // }); - test("on container mounted call back",()=>{ const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.create({ diff --git a/tests/core/external/reveal/reveal-container.test.ts b/tests/core/external/reveal/reveal-container.test.ts index 59f39777..e6516402 100644 --- a/tests/core/external/reveal/reveal-container.test.ts +++ b/tests/core/external/reveal/reveal-container.test.ts @@ -63,6 +63,7 @@ const testMetaData: Metadata = { skyflowContainer: { isControllerFrameReady: true, } as unknown as SkyflowContainer, + getSkyflowBearerToken: getBearerToken, }; const testMetaData2: Metadata = { @@ -135,6 +136,23 @@ describe("Reveal Container Class", () => { expect(testRevealContainer1).toHaveProperty("reveal"); expect(testRevealContainer1).toHaveProperty("type"); }); + test("reveal when elment is empty when skyflow ready", async() => { + const errPromise = testRevealContainer2.reveal() + await expect(errPromise).rejects.toEqual(new Error(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?.error).toBeDefined(); + expect(error?.error?.code).toEqual(400); + expect(error?.error?.description).toEqual( + logs.errorLogs.NO_ELEMENTS_IN_REVEAL + ); + }); + }); test("create() will return a Reveal Element", () => { const testRevealElement = testRevealContainer1.create(testRecord); @@ -388,11 +406,12 @@ describe("Reveal Container Class", () => { done(); expect(error).toBeDefined(); expect(error.errors).toBeDefined(); - expect(error.errors![0].error.code).toEqual(400); - expect(error.errors![0].error.description).toEqual( + expect(error.errors![0].code).toEqual(400); + expect(error.errors![0].description).toEqual( logs.errorLogs.REVEAL_ELEMENT_ERROR_STATE ); }); + element.resetError(); }); test("reveal before skyflow frame ready", (done) => { @@ -403,35 +422,10 @@ describe("Reveal Container Class", () => { done(); expect(error).toBeDefined(); expect(error.errors).toBeDefined(); - expect(error.errors![0].error.code).toEqual(400); - expect(error.errors![0].error.description).toEqual( + expect(error.errors![0].code).toEqual(400); + expect(error.errors![0].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.js b/tests/core/external/reveal/reveal-element.test.js index b7dc57d7..f0c380cd 100644 --- a/tests/core/external/reveal/reveal-element.test.js +++ b/tests/core/external/reveal/reveal-element.test.js @@ -438,22 +438,13 @@ describe("Reveal Element Class", () => { 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(error).toEqual({ errors: { skyflowId:'1244', error: "No Records Found", column: "Not column" } }); }); 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: [] } }); + emitCb({ errors: { skyflowId:'1244', error: "No Records Found", column: "Not column" } }); }); test("Mount Method with ready to mount false", () => { diff --git a/tests/core/external/reveal/reveal-element.test.ts b/tests/core/external/reveal/reveal-element.test.ts index 35ecdb71..fa0872bf 100644 --- a/tests/core/external/reveal/reveal-element.test.ts +++ b/tests/core/external/reveal/reveal-element.test.ts @@ -8,6 +8,7 @@ import { ELEMENT_EVENTS_TO_CLIENT, REVEAL_TYPES, REVEAL_ELEMENT_OPTIONS_TYPES, + ElementType, } from "../../../../src/core/constants"; import RevealElement from "../../../../src/core/external/reveal/reveal-element"; import SkyflowContainer from "../../../../src/core/external/skyflow-container"; @@ -95,6 +96,7 @@ const metaData: Metadata = { clientDomain: clientDomain, }, }, + getSkyflowBearerToken: getBearerToken, skyflowContainer: { isControllerFrameReady: true, } as unknown as SkyflowContainer, @@ -193,6 +195,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -209,6 +212,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: true, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -256,6 +260,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: true, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -303,6 +308,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: true, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -380,6 +386,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: true, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -435,6 +442,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: true, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -489,6 +497,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: true, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -517,45 +526,17 @@ describe("Reveal Element Class", () => { }); 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, + testRevealElement.renderFile().then( + data => console.log('data', data) + ).catch ( + (error) => { + expect(error).toEqual({ errors: { skyflowId:'1244', error: "No Records Found", column: "Not column" } }); }); + + 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: [], - }, - }); + emitCb({ errors: { skyflowId:'1244', error: "No Records Found", column: "Not column" } }); }); test("Mount method with ready to mount false", () => { @@ -567,6 +548,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -599,6 +581,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -631,6 +614,7 @@ describe("Reveal Element Class", () => { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter, + type: ContainerType.REVEAL }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } @@ -647,7 +631,8 @@ describe("Reveal Element Methods", () => { }, undefined, metaData, - { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter }, + { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter, type: ContainerType.REVEAL + }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } ); @@ -690,7 +675,8 @@ describe("Reveal Element Methods", () => { }, undefined, metaData, - { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter }, + { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter, type: ContainerType.REVEAL + }, elementId, { logLevel: LogLevel.ERROR, env: Env.PROD } ); diff --git a/tests/core/internal/composable-frame-element-init.test.js b/tests/core/internal/composable-frame-element-init.test.js new file mode 100644 index 00000000..1970f194 --- /dev/null +++ b/tests/core/internal/composable-frame-element-init.test.js @@ -0,0 +1,1231 @@ +// import FrameElementInit from '../../../src/core/internal/frame-element-init'; +import RevealComposableFrameElementInit from '../../../src/core/internal/composable-frame-element-init'; +import { ELEMENT_EVENTS_TO_IFRAME, COMPOSABLE_REVEAL, ELEMENT_EVENTS_TO_CLIENT, FRAME_REVEAL, REVEAL_TYPES } from '../../../src/core/constants'; +import bus from 'framebus'; +import SkyflowError from '../../../src/libs/skyflow-error'; +import { fetchRecordsByTokenIdComposable, formatRecordsForClientComposable } from '../../../src/core-utils/reveal'; + +// Create a mock function that can be controlled per test +const mockFetchRecordsByTokenIdComposable = jest.fn(); + +// mock fetchRecordsByTokenIdComposable with a jest.fn() so we can control it per test +jest.mock('../../../src/core-utils/reveal', () => { + const actual = jest.requireActual('../../../src/core-utils/reveal'); + return { + ...actual, + fetchRecordsByTokenIdComposable: (...args) => mockFetchRecordsByTokenIdComposable(...args), + }; +}); + + +const stylesOptions = { + inputStyles: { + base: { + border: "1px solid #eae8ee", + padding: "10px 16px", + borderRadius: "4px", + color: "#1d1d1d", + marginTop: "4px" + }, + complete: { + color: "#4caf50" + }, + empty: {}, + focus: {}, + invalid: { + color: "#f44336" + }, + cardIcon: { + position: "absolute", + left: "8px", + top: "calc(50% - 10px)", + }, + copyIcon: { + position: "absolute", + right: "8px", + top:"calc(50% - 10px)", + cursor: "pointer", + } + }, + labelStyles: { + base: { + fontSize: "16px", + fontWeight: "bold" + } + }, + errorTextStyles: { + base: { + color: "#f44336" + } + } +}; +const element = { + elementName: "element:group:W29iamVjdCBPYmplY3Rd", + rows: [{ + elements: [ + { + elementType: 'REVEAL', + elementName: `reveal-composable:123`, + name: `reveal-composable:123`, + table: 'patients', + column: 'card_number', + token: 'skyflow-id-1', + elementId: 'element-id-1', + ...stylesOptions + }, + ] + }], + errorTextStyles:{ + base: { + color: "#f44336", + fontFamily:'Inter', + }, + global:{ + '@import':'https://font-url.com/Inter' + } + }, + clientDomain: 'http://localhost.com' +} +const element2 = { + elementName: "element:group:W29iamVjdCBPYmplY3Rd", + rows: [{ + elements: [ + { + elementType: 'REVEAL', + elementName: `reveal-composable:123`, + name: `reveal-composable:123`, + table: 'patients', + column: 'card_number', + token: 'skyflow-id-1', + elementId: 'element-id-1', + ...stylesOptions + }, + { + elementType: 'REVEAL', + elementName: `reveal-composable:124`, + name: `reveal-composable:124`, + table: 'patients', + column: 'card_number', + token: 'skyflow-id-2', + elementId: 'element-id-2', + ...stylesOptions + }, + ] + }], + errorTextStyles:{ + base: { + color: "#f44336", + fontFamily:'Inter', + }, + global:{ + '@import':'https://font-url.com/Inter' + } + }, + clientDomain: 'http://localhost.com' +} +const on = jest.fn(); +const emit = jest.fn(); +describe('composableFrameElementInit Additional Test Cases', () => { + let emitSpy; + let windowSpy; + let targetSpy; + + beforeEach(() => { + windowSpy = jest.spyOn(global, 'window', 'get'); + jest.clearAllMocks() + emitSpy = jest.spyOn(bus, 'emit'); + targetSpy = jest.spyOn(bus, 'target'); + targetSpy.mockReturnValue({ + on, + emit + }); + + // Set default successful response for fetchRecordsByTokenIdComposable + mockFetchRecordsByTokenIdComposable.mockResolvedValue({ + records: [{ + token: 'skyflow-id-1', + value: '4111111111111111', + valueType: 'STRING', + frameId: 'reveal-composable:123', + }] + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('should handle missing window name gracefully', () => { + windowSpy = jest.spyOn(global, 'window', 'get'); + windowSpy.mockImplementation(() => ({ + name: ``, + location: { + href: `http://localhost/?${btoa(JSON.stringify({record:element, clientJSON: {metaData: {clientDomain: 'http://localhost.com'}}}))}`, + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), + })) + // const onSpy = jest.spyOn(bus, 'on'); + const frameElement = new RevealComposableFrameElementInit() + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should handle invalid base64 encoded URL data', () => { + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:123:ERROR:`, + location: { href: 'http://localhost/?invalid_base64_data' }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).toThrowError({'message':'The string to be decoded contains invalid characters.'}); + }); + + test('should correctly bind multiple event listeners', () => { + const onSpy = jest.spyOn(bus, 'on'); + windowSpy = jest.spyOn(global, 'window', 'get'); + const id = `${COMPOSABLE_REVEAL}:123:ERROR:` + windowSpy.mockImplementation(() => ({ + name: id, // Fix constant reference + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' } }))}`, + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })); + + RevealComposableFrameElementInit.startFrameElement(); + window.dispatchEvent(new MessageEvent('message', { + data: { + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + id, + payload: { + name: id, + }, + }, + origin: 'http://localhost.com', + })); + + expect(onSpy).toHaveBeenCalledWith(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + id, expect.any(Function) + ) + expect(onSpy).toHaveBeenCalledWith(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + id, expect.any(Function)); + }); + + test('should correctly extract record and metadata from URL', () => { + const onSpy = jest.spyOn(bus, 'on'); + windowSpy = jest.spyOn(global, 'window', 'get'); + const id = `${COMPOSABLE_REVEAL}:123:ERROR:` + windowSpy.mockImplementation(() => ({ + name: id, // Fix constant reference + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' } }))}`, + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should correctly initialize composable frame elements', () => { + RevealComposableFrameElementInit.group = []; + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:group:123:ERROR:`, + location: { + href: `http://localhost/?${btoa( + JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' } }) + )}` + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should throw error for incorrect element type', () => { + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:UNKNOWN_TYPE:123:ERROR:`, + location: { href: `http://localhost/?${btoa(JSON.stringify({ record: {}, metaData: {} }))}` } + })); + expect(() => RevealComposableFrameElementInit.startFrameElement()).toThrow(TypeError); + }); + + test('should handle malformed window name', () => { + windowSpy.mockImplementation(() => ({ + name: 'INVALID_NAME_FORMAT', + location: { href: 'http://localhost/' } + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).toThrow(TypeError); + }); + + test('should not crash on undefined window object', () => { + windowSpy.mockImplementation(() => undefined); + + expect(() => RevealComposableFrameElementInit.startFrameElement('')).toThrow(TypeError); + }); + + test('composable reveal frame element init reveal events', () => { + const onSpy = jest.spyOn(bus, 'on'); + windowSpy = jest.spyOn(global, 'window', 'get'); + const id = `${COMPOSABLE_REVEAL}:123:ERROR:` + const containerId = '123'; + windowSpy.mockImplementation(() => ({ + name: id, // Fix constant reference + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' }, containerId:containerId }))}`, + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + }, + // addEventListener: jest.fn(), + // dispatchEvent: jest.fn(), + }, + addEventListener: jest.fn(), + dispatchEvent: jest.fn(), + postMessage: jest.fn(), + })); + + // expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + RevealComposableFrameElementInit.startFrameElement(); + window.dispatchEvent(new MessageEvent('message', { + data:{ + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context:{ + vaultId: 'vault-id-1', + }, + data:{ + elementIds: ['element-id-1'], + type: REVEAL_TYPES.REVEAL, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig:{ + clientDomain: 'http://localhost.com', + uuid: 'uuid-1', + } + } + })); + + }); + + test('should handle HEIGHT_CALLBACK_COMPOSABLE event', () => { + const onSpy = jest.spyOn(bus, 'on'); + windowSpy = jest.spyOn(global, 'window', 'get'); + const id = `${COMPOSABLE_REVEAL}:123:ERROR:` + const containerId = '123'; + const postMessageSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, containerId:containerId }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: jest.fn(), + dispatchEvent: jest.fn(), + postMessage: jest.fn(), + })); + + RevealComposableFrameElementInit.startFrameElement(); + + // Trigger HEIGHT event + window.dispatchEvent(new MessageEvent('message', { + data:{ + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + id, + } + })); + + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + id, + data: expect.objectContaining({ + name: id, + }), + }), + 'http://localhost.com' + ); + }); + + test('should handle HEIGHT_CALLBACK_COMPOSABLE message event', () => { + const onSpy = jest.spyOn(bus, 'on'); + windowSpy = jest.spyOn(global, 'window', 'get'); + const id = `${COMPOSABLE_REVEAL}:456:ERROR:` + const containerId = '456'; + const postMessageSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, containerId:containerId }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: jest.fn(), + dispatchEvent: jest.fn(), + postMessage: jest.fn(), + })); + + RevealComposableFrameElementInit.startFrameElement(); + + // Trigger HEIGHT_CALLBACK_COMPOSABLE event + window.dispatchEvent(new MessageEvent('message', { + data:{ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + id, + } + })); + + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + id, + data: expect.objectContaining({ + name: id, + }), + }), + 'http://localhost.com' + ); + }); + + test('should handle multiple rows in group element', () => { + const multiRowElement = { + ...element, + rows: [ + { + elements: [ + { + elementType: 'REVEAL', + elementName: `reveal-composable:row1-elem1`, + name: `reveal-composable:row1-elem1`, + table: 'patients', + column: 'first_name', + skyflowID: 'skyflow-id-1', + elementId: 'element-id-1', + ...stylesOptions + } + ] + }, + { + elements: [ + { + elementType: 'REVEAL', + elementName: `reveal-composable:row2-elem1`, + name: `reveal-composable:row2-elem1`, + table: 'patients', + column: 'last_name', + skyflowID: 'skyflow-id-2', + elementId: 'element-id-2', + ...stylesOptions + } + ] + } + ] + }; + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:multi-row:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: multiRowElement, metaData: { clientDomain: 'http://localhost.com' }, containerId: 'multi-row' }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should handle elements with custom spacing', () => { + const elementWithSpacing = { + ...element, + spacing: '10px', + rows: [{ + spacing: '5px', + elements: [ + { + elementType: 'REVEAL', + elementName: `reveal-composable:spaced-elem`, + name: `reveal-composable:spaced-elem`, + table: 'patients', + column: 'card_number', + skyflowID: 'skyflow-id-1', + elementId: 'element-id-1', + ...stylesOptions + } + ] + }] + }; + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:spacing-test:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: elementWithSpacing, metaData: { clientDomain: 'http://localhost.com' }, containerId: 'spacing-test' }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should handle elements with alignItems and justifyContent', () => { + const elementWithAlignment = { + ...element, + alignItems: 'center', + justifyContent: 'space-between', + rows: [{ + alignItems: 'flex-start', + justifyContent: 'flex-end', + elements: [ + { + elementType: 'REVEAL', + elementName: `reveal-composable:aligned-elem`, + name: `reveal-composable:aligned-elem`, + table: 'patients', + column: 'card_number', + skyflowID: 'skyflow-id-1', + elementId: 'element-id-1', + ...stylesOptions + } + ] + }] + }; + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:alignment-test:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: elementWithAlignment, metaData: { clientDomain: 'http://localhost.com' }, containerId: 'alignment-test' }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should emit MOUNTED event on initialization', () => { + const containerId = '123'; + const postMessageSpy = jest.fn(); + const addEventListenerSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:123:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, containerId: containerId }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: addEventListenerSpy, + })); + + RevealComposableFrameElementInit.startFrameElement(); + + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + containerId, + }), + 'http://localhost.com' + ); + }); + + test('should handle RENDER_FILE type reveal calls', () => { + const onSpy = jest.spyOn(bus, 'on'); + windowSpy = jest.spyOn(global, 'window', 'get'); + const id = `${COMPOSABLE_REVEAL}:file-render:ERROR:` + const containerId = 'file-render'; + const addEventListenerSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, containerId: containerId }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: addEventListenerSpy, + dispatchEvent: jest.fn(), + postMessage: jest.fn(), + })); + + RevealComposableFrameElementInit.startFrameElement(); + + window.dispatchEvent(new MessageEvent('message', { + data:{ + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context:{ + vaultId: 'vault-id-1', + }, + data:{ + elementIds: ['element-id-1'], + type: REVEAL_TYPES.RENDER_FILE, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig:{ + clientDomain: 'http://localhost.com', + uuid: 'uuid-1', + } + } + })); + + expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + test('should handle reveal data with multiple element IDs', () => { + const multiElementRecord = { + ...element, + rows: [{ + elements: [ + { + elementType: 'REVEAL', + elementName: `reveal-composable:elem-1`, + name: `reveal-composable:elem-1`, + table: 'patients', + column: 'card_number', + skyflowID: 'skyflow-id-1', + elementId: 'element-id-1', + token: 'token-1', + ...stylesOptions + }, + { + elementType: 'REVEAL', + elementName: `reveal-composable:elem-2`, + name: `reveal-composable:elem-2`, + table: 'patients', + column: 'cvv', + skyflowID: 'skyflow-id-2', + elementId: 'element-id-2', + token: 'token-2', + ...stylesOptions + } + ] + }] + }; + + const containerId = 'multi-elem'; + const addEventListenerSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: multiElementRecord, clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, containerId: containerId }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: addEventListenerSpy, + dispatchEvent: jest.fn(), + postMessage: jest.fn(), + })); + + RevealComposableFrameElementInit.startFrameElement(); + + window.dispatchEvent(new MessageEvent('message', { + data:{ + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context:{ + vaultId: 'vault-id-1', + }, + data:{ + elementIds: [{frameId: 'reveal-composable:elem-1'}, {frameId: 'reveal-composable:elem-2'}], + type: REVEAL_TYPES.REVEAL, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig:{ + clientDomain: 'http://localhost.com', + uuid: 'uuid-1', + } + } + })); + + expect(addEventListenerSpy).toHaveBeenCalled(); + }); + + test('should handle empty rows array', () => { + const emptyRowsElement = { + ...element, + rows: [] + }; + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:empty-rows:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: emptyRowsElement, metaData: { clientDomain: 'http://localhost.com' }, containerId: 'empty-rows' }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should handle missing styles in element', () => { + const noStylesElement = { + elementName: "element:group:test", + rows: [{ + elements: [ + { + elementType: 'REVEAL', + elementName: `reveal-composable:no-styles`, + name: `reveal-composable:no-styles`, + table: 'patients', + column: 'card_number', + skyflowID: 'skyflow-id-1', + elementId: 'element-id-1', + } + ] + }] + }; + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:no-styles:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: noStylesElement, metaData: { clientDomain: 'http://localhost.com' }, containerId: 'no-styles' }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should handle global styles in errorTextStyles', () => { + const globalStylesElement = { + ...element, + errorTextStyles: { + base: { + color: "#f44336", + }, + global: { + '@import': 'https://fonts.googleapis.com/css2?family=Roboto&display=swap' + } + } + }; + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:global-styles:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: globalStylesElement, metaData: { clientDomain: 'http://localhost.com' }, containerId: 'global-styles' }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should listen to HEIGHT event and respond with height data', () => { + const onSpy = jest.spyOn(bus, 'on'); + const containerId = 'height-test'; + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' }, containerId: containerId }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + RevealComposableFrameElementInit.startFrameElement(); + + // Verify HEIGHT listener is registered + expect(onSpy).toHaveBeenCalledWith( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`, + expect.any(Function) + ); + }); + + test('should handle reveal call with empty elementIds', () => { + const containerId = 'empty-ids'; + const addEventListenerSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, containerId: containerId }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: addEventListenerSpy, + dispatchEvent: jest.fn(), + postMessage: jest.fn(), + })); + + RevealComposableFrameElementInit.startFrameElement(); + + window.dispatchEvent(new MessageEvent('message', { + data:{ + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context:{ + vaultId: 'vault-id-1', + }, + data:{ + elementIds: [], + type: REVEAL_TYPES.REVEAL, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig:{ + clientDomain: 'http://localhost.com', + uuid: 'uuid-1', + } + } + })); + + expect(addEventListenerSpy).toHaveBeenCalled(); + }); + + test('should handle rows with multiple elements', () => { + const multiElementRowElement = { + ...element, + rows: [{ + elements: [ + { + elementType: 'REVEAL', + elementName: `reveal-composable:elem-1`, + name: `reveal-composable:elem-1`, + table: 'patients', + column: 'first_name', + skyflowID: 'skyflow-id-1', + elementId: 'element-id-1', + ...stylesOptions + }, + { + elementType: 'REVEAL', + elementName: `reveal-composable:elem-2`, + name: `reveal-composable:elem-2`, + table: 'patients', + column: 'last_name', + skyflowID: 'skyflow-id-2', + elementId: 'element-id-2', + ...stylesOptions + }, + { + elementType: 'REVEAL', + elementName: `reveal-composable:elem-3`, + name: `reveal-composable:elem-3`, + table: 'patients', + column: 'email', + skyflowID: 'skyflow-id-3', + elementId: 'element-id-3', + ...stylesOptions + } + ] + }] + }; + + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:multi-elem-row:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: multiElementRowElement, metaData: { clientDomain: 'http://localhost.com' }, containerId: 'multi-elem-row' }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should handle missing containerId in URL params', () => { + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:missing-container:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' } }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('should handle missing context in URL params', () => { + windowSpy.mockImplementation(() => ({ + name: `${COMPOSABLE_REVEAL}:missing-context:ERROR:`, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' }, containerId: 'missing-context' }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + expect(() => RevealComposableFrameElementInit.startFrameElement()).not.toThrow(); + }); + + test('reveal success call', async () => { + const containerId = 'unrelated-msg-test'; + const id = `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`; + const postMessageSpy = jest.fn().mockImplementation(() => { + }); + let messageHandler; + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, + containerId: containerId, + }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: (event, handler) => { + if (event === 'message') { + messageHandler = handler; + } + }, + })); + + RevealComposableFrameElementInit.startFrameElement(); + + // Clear initial calls + postMessageSpy.mockClear(); + + // Send unrelated message + if (messageHandler) { + messageHandler({ + data:{ + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context:{ + vaultId: 'vault-id-1', + }, + data:{ + elementIds: [{frameId:'reveal-composable:123'}], + type: REVEAL_TYPES.REVEAL, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig:{ + clientDomain: 'http://localhost.com', + uuid: 'uuid-1', + } + } + }); + // Should not call postMessage for unrelated events + expect(postMessageSpy).not.toHaveBeenCalled(); + } + }); + + test('should handle reveal success response with records', async () => { + const containerId = 'reveal-success-test'; + const id = `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`; + const postMessageSpy = jest.fn(); + let messageHandler; + + // Mock successful response + mockFetchRecordsByTokenIdComposable.mockResolvedValue({ + records: [{ + token: 'skyflow-id-1', + value: '4111111111111111', + valueType: 'STRING', + frameId: 'reveal-composable:123', + }] + }); + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, + containerId: containerId, + }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: (event, handler) => { + if (event === 'message') { + messageHandler = handler; + } + }, + })); + + RevealComposableFrameElementInit.startFrameElement(); + + // Clear initial calls + postMessageSpy.mockClear(); + + // Send reveal message + if (messageHandler) { + messageHandler({ + data:{ + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context:{ + vaultId: 'vault-id-1', + }, + data:{ + elementIds: [{frameId:'reveal-composable:123'}], + type: REVEAL_TYPES.REVEAL, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig:{ + clientDomain: 'http://localhost.com', + uuid: 'uuid-1', + authToken: 'test-token', + } + } + }); + + // Wait for async operations to complete + await new Promise(resolve => setTimeout(resolve, 100)); + + // Should call postMessage with REVEAL_RESPONSE_READY + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + containerId, + }), + 'http://localhost.com' + ); + } + }); + + test('should handle reveal error response', async () => { + const containerId = 'reveal-error-test'; + const id = `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`; + const postMessageSpy= jest.fn().mockImplementation((data) => { + if (data.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + containerId) { + expect(data).toMatchObject({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + containerId, + data: { + errors: [{ + error: { + code: '404', + description: 'Token not found', + }, + } + ], + success: [{ + token: '', + valueType: '', + }] + }, + }); + } + }); + let messageHandler; + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element2, + clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, + containerId: containerId, + }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: (event, handler) => { + if (event === 'message') { + messageHandler = handler; + } + }, + })); + + RevealComposableFrameElementInit.startFrameElement(); + + // Clear initial calls + postMessageSpy.mockClear(); + // Mock error response + mockFetchRecordsByTokenIdComposable.mockRejectedValue({ + errors: [{ + token: 'skyflow-id-1', + error: { + code: '404', + description: 'Token not found', + }, + frameId: 'reveal-composable:124', + }], + records: [{ + token: 'skyflow-id-1', + value: '4111111111111111', + valueType: 'STRING', + frameId: 'reveal-composable:123', + }] + }); + // Send reveal message + if (messageHandler) { + messageHandler({ + data:{ + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context:{ + vaultId: 'vault-id-1', + }, + data:{ + elementIds: [{frameId:'reveal-composable:123'}, {frameId:'reveal-composable:124'}], + type: REVEAL_TYPES.REVEAL, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig:{ + clientDomain: 'http://localhost.com', + uuid: 'uuid-1', + authToken: 'test-token', + } + } + }); + + // Wait for async operations to complete + await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(reject => setTimeout(reject, 100)); + // Should call postMessage with REVEAL_RESPONSE_READY even for errors + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + containerId, + }), + 'http://localhost.com' + ); + } + }); + + test('reveal call - ignore unrelated messages', async () => { + const containerId = 'unrelated-msg-test'; + const id = `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`; + const postMessageSpy = jest.fn().mockImplementation(() => { + }); + let messageHandler; + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, + containerId: containerId, + }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: (event, handler) => { + if (event === 'message') { + messageHandler = handler; + } + }, + })); + + RevealComposableFrameElementInit.startFrameElement(); + + // Clear initial calls + postMessageSpy.mockClear(); + + // Send unrelated message + if (messageHandler) { + messageHandler({ + data:{ + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context:{ + vaultId: 'vault-id-1', + }, + data:{ + elementIds: [{frameId:'reveal-composable:123'}], + type: REVEAL_TYPES.REVEAL, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig:{ + clientDomain: 'http://localhost.com', + uuid: 'uuid-1', + } + } + }); + // Should not call postMessage for unrelated events + expect(postMessageSpy).not.toHaveBeenCalled(); + } + }); + + test('should handle window message with missing data fields gracefully', () => { + const containerId = 'missing-data-test'; + const id = `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`; + const postMessageSpy = jest.fn(); + let messageHandler; + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + clientJSON: { metaData: { clientDomain: 'http://localhost.com' }}, + containerId: containerId + }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: (event, handler) => { + if (event === 'message') { + messageHandler = handler; + } + }, + })); + + RevealComposableFrameElementInit.startFrameElement(); + + postMessageSpy.mockClear(); + + // Send message with missing data + if (messageHandler) { + expect(() => { + messageHandler({ + data: null + }); + }).not.toThrow(); + + expect(() => { + messageHandler({}); + }).not.toThrow(); + } + }); +}); diff --git a/tests/core/internal/frame-element-init.test.js b/tests/core/internal/frame-element-init.test.js index 5a696f65..7fac34fa 100644 --- a/tests/core/internal/frame-element-init.test.js +++ b/tests/core/internal/frame-element-init.test.js @@ -1,7 +1,59 @@ import FrameElementInit from '../../../src/core/internal/frame-element-init'; -import { ELEMENT_EVENTS_TO_IFRAME, FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT } from '../../../src/core/constants'; +import { ELEMENT_EVENTS_TO_IFRAME, FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, ElementType } from '../../../src/core/constants'; import bus from 'framebus'; import SkyflowError from '../../../src/libs/skyflow-error'; +import * as helpers from '../../../src/utils/helpers'; +import Client from '../../../src/client'; +import IFrameFormElement from '../../../src/core/internal/iframe-form'; + +// Helper to flush pending microtasks (Promise.allSettled resolution) deterministically +const flushPromises = async (cycles = 3) => { + for (let i = 0; i < cycles; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Promise.resolve(); + } +}; + +jest.mock('../../../src/utils/helpers', () => { + const actual = jest.requireActual('../../../src/utils/helpers'); + return { + ...actual, + fileValidation: (...args) => {return true}, + vaildateFileName: () => true, + generateUploadFileName: (name) => `generated_${name}`, + }; +}); +const mockFile1 = new File(['file content 1'], 'test-file-1.txt', { type: 'text/plain' }); +// Create a mock FileList +const mockFileList = mockFile1; +// { +// 0: mockFile1, +// length: 1, +// item: function(index) { return this[index] || null; }, +// [Symbol.iterator]: function* () { +// for (let i = 0; i < this.length; i++) { +// yield this[i]; +// } +// } +// }; +// jest.mock('../../../src/core/internal/iframe-form', () => { +// const actual = jest.requireActual('../../../src/core/internal/iframe-form'); + +// return { +// __esModule: true, +// default: class MockIFrameFormElement extends actual.default { +// constructor(...args) { +// super(...args); +// // Override state after construction +// this.state = { +// value: mockFileList, // This would still have scope issues +// name: 'file_upload', +// // ... rest of state +// }; +// } +// } +// }; +// }); const stylesOptions = { inputStyles: { @@ -54,6 +106,13 @@ const element = { table: 'patients', column: 'card_number', ...stylesOptions + }, + { + elementType: ElementType.MULTI_FILE_INPUT, + elementName: `element:MULTI_FILE_INPUT:123`, + table: 'patients', + column: 'file_uploads', + ...stylesOptions } ] }], @@ -96,18 +155,31 @@ describe('FrameElementInit Additional Test Cases', () => { name: ``, location: { href: `http://localhost/?${btoa(JSON.stringify({record:element, metaData: {clientDomain: 'http://localhost.com'}}))}`, - } + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), })) const onSpy = jest.spyOn(bus, 'on'); const frameElement = new FrameElementInit('CARD_NUMBER') expect(() => FrameElementInit.startFrameElement('CARD_NUMBER')).not.toThrow(); - console.log('mock calsss====', on.mock.calls) }); test('should handle invalid base64 encoded URL data', () => { windowSpy.mockImplementation(() => ({ name: `${FRAME_ELEMENT}:CARD_NUMBER:123:ERROR:`, - location: { href: 'http://localhost/?invalid_base64_data' } + location: { href: 'http://localhost/?invalid_base64_data' }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), })); expect(() => FrameElementInit.startFrameElement()).toThrowError({'message':'The string to be decoded contains invalid characters.'}); @@ -121,7 +193,14 @@ describe('FrameElementInit Additional Test Cases', () => { name: id, // Fix constant reference location: { href: `http://localhost/?${btoa(JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' } }))}`, - } + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), })); FrameElementInit.startFrameElement(); @@ -143,7 +222,14 @@ describe('FrameElementInit Additional Test Cases', () => { windowSpy.mockImplementation(() => ({ name: `${FRAME_ELEMENT}:CARD_NUMBER:123:ERROR:`, - location: { href: `http://localhost/?${btoa(JSON.stringify(mockData))}` } + location: { href: `http://localhost/?${btoa(JSON.stringify(mockData))}` }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), })); expect(() => FrameElementInit.startFrameElement()).not.toThrow(); @@ -158,7 +244,14 @@ describe('FrameElementInit Additional Test Cases', () => { href: `http://localhost/?${btoa( JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' } }) )}` - } + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), })); expect(() => FrameElementInit.startFrameElement()).not.toThrow(); @@ -179,7 +272,14 @@ describe('FrameElementInit Additional Test Cases', () => { href: `http://localhost/?${btoa( JSON.stringify({ record: element, metaData: { clientDomain: 'http://localhost.com' } }) )}` - } + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), })); FrameElementInit.startFrameElement(); @@ -204,4 +304,500 @@ describe('FrameElementInit Additional Test Cases', () => { expect(() => FrameElementInit.startFrameElement('')).toThrow(TypeError); }); -}); + + test('should call window.parent.postMessage on HEIGHT_CALLBACK event', () => { + const id = `${FRAME_ELEMENT}:CARD_NUMBER:123:ERROR:`; + const postMessageSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + metaData: { clientDomain: 'http://localhost.com' } + }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: jest.fn(), + })); + + FrameElementInit.startFrameElement(); + + // Verify that postMessage was called with HEIGHT_CALLBACK + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + id, + data: expect.objectContaining({ + name: id, + height: expect.any(Number), + }), + }), + 'http://localhost.com' + ); + }); + + test('should add window message event listener', () => { + const id = `${FRAME_ELEMENT}:CARD_NUMBER:123:ERROR:`; + const addEventListenerSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + metaData: { clientDomain: 'http://localhost.com' } + }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: addEventListenerSpy, + })); + + FrameElementInit.startFrameElement(); + + // Verify that addEventListener was called for 'message' event + expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + // test('should handle HEIGHT message event and post HEIGHT_CALLBACK', () => { + // const id = `${FRAME_ELEMENT}:CARD_NUMBER:height-test:ERROR:`; + // const postMessageSpy = jest.fn(); + // let messageHandler; + // let windowMock; + // const addEventListenerSpy = jest.fn((event, handler) => { + // if (event === 'message') { + // messageHandler = handler; + // } + // }); + + // windowSpy.mockImplementation(() => { + // windowMock = { + // name: id, + // location: { + // href: `http://localhost/?${btoa(JSON.stringify({ + // record: element, + // metaData: { clientDomain: 'http://localhost.com' } + // }))}`, + // }, + // parent: { + // postMessage: postMessageSpy, + // }, + // addEventListener: addEventListenerSpy, + // }; + // return windowMock; + // }); + + // FrameElementInit.startFrameElement(); + + // // Clear initial postMessage calls + // postMessageSpy.mockClear(); + + // // Simulate HEIGHT message event + // if (messageHandler) { + // // Ensure window mock is still available when handler executes + // windowSpy.mockImplementation(() => windowMock); + + // messageHandler({ + // data: { + // name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + id, + // } + // }); + + // // Verify postMessage was called in response to HEIGHT event + // expect(postMessageSpy).toHaveBeenCalledWith( + // expect.objectContaining({ + // type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + id, + // data: expect.objectContaining({ + // name: id, + // height: expect.any(Number), + // }), + // }), + // 'http://localhost.com' + // ); + // } + // }); + + test('should register HEIGHT bus event listener', () => { + const id = `${FRAME_ELEMENT}:CARD_NUMBER:123:ERROR:`; + const onSpy = jest.spyOn(bus, 'on'); + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + metaData: { clientDomain: 'http://localhost.com' } + }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + FrameElementInit.startFrameElement(); + + // Verify HEIGHT listener is registered + expect(onSpy).toHaveBeenCalledWith( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + id, + expect.any(Function) + ); + }); + + test('should ignore unrelated window messages', () => { + const id = `${FRAME_ELEMENT}:CARD_NUMBER:unrelated-test:ERROR:`; + const postMessageSpy = jest.fn(); + let messageHandler; + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + metaData: { clientDomain: 'http://localhost.com' } + }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: (event, handler) => { + if (event === 'message') { + messageHandler = handler; + } + }, + })); + + FrameElementInit.startFrameElement(); + postMessageSpy.mockClear(); + + // Send unrelated message + if (messageHandler) { + messageHandler({ + data: { + name: 'UNRELATED_EVENT', + type: 'SOME_OTHER_TYPE', + } + }); + + // Should not call postMessage for unrelated events + expect(postMessageSpy).not.toHaveBeenCalled(); + } + }); + + test('should handle window message with missing data gracefully', () => { + const id = `${FRAME_ELEMENT}:CARD_NUMBER:missing-data:ERROR:`; + const postMessageSpy = jest.fn(); + let messageHandler; + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + metaData: { clientDomain: 'http://localhost.com' } + }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: (event, handler) => { + if (event === 'message') { + messageHandler = handler; + } + }, + })); + + FrameElementInit.startFrameElement(); + postMessageSpy.mockClear(); + + // Send message with missing data + if (messageHandler) { + expect(() => { + messageHandler({ data: null }); + }).not.toThrow(); + + expect(() => { + messageHandler({}); + }).not.toThrow(); + + // Should not call postMessage for invalid events + expect(postMessageSpy).not.toHaveBeenCalled(); + } + }); + + test('should respond to HEIGHT bus event with callback', () => { + const id = `${FRAME_ELEMENT}:CARD_NUMBER:bus-height:ERROR:`; + const callbackSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + metaData: { clientDomain: 'http://localhost.com' } + }))}`, + }, + parent: { + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), + })); + + FrameElementInit.startFrameElement(); + + // Get the HEIGHT event listener callback + const heightCallback = on.mock.calls.find(call => + call[0] === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + id + )?.[1]; + + // Trigger the HEIGHT event with callback + if (heightCallback) { + heightCallback({}, callbackSpy); + + // Verify callback was called with height data + expect(callbackSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: id, + height: expect.any(Number), + }) + ); + } + }); + + test('should post HEIGHT_CALLBACK on BLUR event for composable container', () => { + const composableElement = { + ...element, + rows: [{ + elements: [{ + elementType: 'CARD_NUMBER', + elementName: `element:CARD_NUMBER:composable-blur`, + table: 'patients', + column: 'card_number', + ...stylesOptions + }] + }] + }; + + const id = `${FRAME_ELEMENT}:group:composable-blur:ERROR:`; + const postMessageSpy = jest.fn(); + + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: composableElement, + metaData: { clientDomain: 'http://localhost.com' } + }))}`, + }, + parent: { + postMessage: postMessageSpy, + }, + addEventListener: jest.fn(), + })); + + FrameElementInit.startFrameElement(); + + // The HEIGHT_CALLBACK should be posted on initialization + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + id, + }), + 'http://localhost.com' + ); + }); + test('should handle multi file upload message event error case', async () => { + const composableElement = { + ...element, + rows: [{ + elements: [{ + elementType: 'CARD_NUMBER', + elementName: `element:CARD_NUMBER:123`, + table: 'patients', + column: 'card_number', + ...stylesOptions + }, + { + elementType: ElementType.MULTI_FILE_INPUT, + elementName: `element:MULTI_FILE_INPUT:123`, + table: 'patients', + column: 'file_uploads', + ...stylesOptions + }] + }] + }; + + const id = `${FRAME_ELEMENT}:group:123:ERROR:`; + const postMessageSpy = jest.fn(); + let messageHandler; + let windowMock; + + windowSpy.mockImplementation(() => { + windowMock = { + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: composableElement, + metaData: { clientDomain: 'http://localhost.com' } + }))}`, + }, + parent: { + postMessage: postMessageSpy, + addEventListener: jest.fn(), + }, + addEventListener: (event, handler) => { + if (event === 'message') { + messageHandler = handler; + } + }, + dispatchEvent: jest.fn(), + }; + return windowMock; + }); + + FrameElementInit.startFrameElement(); + + // Clear initialization postMessage calls + postMessageSpy.mockClear(); + + // Ensure window mock is available when handler executes + windowSpy.mockImplementation(() => windowMock); + + // Simulate multi file upload message event + if (messageHandler) { + messageHandler(new MessageEvent('message', { + data: { + name: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:element:MULTI_FILE_INPUT:123`, + clientConfig: { + vaultId: 'vault123', + vaultURL: 'https://vaulturl.com', + authToken: 'token123', + uuid: 'uuid123', + }, + options: { + // Additional metadata for file upload + } + } + })); + + // Wait for async operations + await new Promise(resolve => setTimeout(resolve, 100)); + + // Verify window.parent.postMessage was called with MULTIPLE_UPLOAD_FILES_RESPONSE + expect(postMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:element:MULTI_FILE_INPUT:123`, + data: {"error": "No files selected"}, // Response data (success or error) + }), + 'http://localhost.com' + ); + } + }); + // test('should handle multi file upload message event with success', async () => { + // const composableElement = { + // ...element, + // rows: [{ + // elements: [{ + // elementType: ElementType.MULTI_FILE_INPUT, + // elementName: 'element:MULTI_FILE_INPUT:123', + // table: 'patients', + // column: 'file_uploads', + // required: false, + // label: 'Upload Files', + // ...stylesOptions, + // }], + // }], + // }; + + // const id = `${FRAME_ELEMENT}:group:123:ERROR:`; + // const postMessageSpy = jest.fn(); + // let messageHandler; + // let windowMock; + + // windowSpy.mockImplementation(() => { + // windowMock = { + // name: id, + // location: { + // href: `http://localhost/?${btoa(JSON.stringify({ + // record: composableElement, + // metaData: { clientDomain: 'http://localhost.com' }, + // }))}`, + // }, + // parent: { + // postMessage: postMessageSpy, + // addEventListener: jest.fn(), + // }, + // addEventListener: (event, handler) => { + // if (event === 'message') messageHandler = handler; + // }, + // dispatchEvent: jest.fn(), + // }; + // return windowMock; + // }); + + // // Mock successful network requests for each file + // const mockRequest = jest.fn().mockResolvedValue({ skyflow_id: 'id1', file: 'test-file-1.txt' }); + // Client.prototype.request = mockRequest; + // Client.prototype.config = { vaultURL: 'https://vaulturl.com', vaultID: 'vault123' }; + + // FrameElementInit.startFrameElement(); + // postMessageSpy.mockClear(); + + // // Find MULTI_FILE_INPUT form element and inject FileList state + // const multiFileElement = FrameElementInit.frameEle.iframeFormList.find( + // (f) => f.iFrameName === 'element:MULTI_FILE_INPUT:123' + // ); + // expect(multiFileElement).toBeTruthy(); + + // if (multiFileElement) { + // Object.defineProperty(multiFileElement, 'state', { + // value: { + // value: mockFileList, // emulate user having selected files + // name: 'file_uploads', + // isRequired: false, + // isValid: true, + // isEmpty: false, + // isComplete: true, + // }, + // configurable: true, + // writable: true, + // }); + // } + + // windowSpy.mockImplementation(() => windowMock); // keep same window for handler + + // if (!messageHandler) throw new Error('messageHandler missing'); + + // messageHandler({ + // data: { + // name: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:element:MULTI_FILE_INPUT:123`, + // clientConfig: { + // vaultId: 'vault123', + // vaultURL: 'https://vaulturl.com', + // authToken: 'token123', + // uuid: 'uuid123', + // }, + // options: {}, + // }, + // }); + + // await flushPromises(5); + + // expect(postMessageSpy).toHaveBeenCalledWith( + // expect.objectContaining({ + // type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:element:MULTI_FILE_INPUT:123`, + // data: expect.objectContaining({ + // fileUploadResponse: expect.arrayContaining([ + // expect.objectContaining({ skyflow_id: 'id1' }), + // ]), + // }), + // }), + // 'http://localhost.com' + // ); + + // // Ensure request called once per file + // expect(mockRequest).toHaveBeenCalledTimes(mockFileList.length); + // }); + }); diff --git a/tests/core/internal/frame-elements.test.js b/tests/core/internal/frame-elements.test.js index 9e3dc6bf..f85bf2bd 100644 --- a/tests/core/internal/frame-elements.test.js +++ b/tests/core/internal/frame-elements.test.js @@ -76,15 +76,23 @@ const element = { describe('test frame elements', () => { let emitSpy; let windowSpy; + let windowSpy1; beforeEach(() => { windowSpy = jest.spyOn(global, 'window', 'get'); windowSpy.mockImplementation(() => ({ name: `${FRAME_ELEMENT}:CARD_NUMBER:123:ERROR:`, location: { href: `http://localhost/?${btoa(JSON.stringify({record:element, metaData: {clientDomain: 'https://demo.com'}}))}`, - } + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + console.log("postMessage called with:", message, targetOrigin, args); + } + }, + addEventListener: jest.fn(), })); - emitSpy = jest.spyOn(bus, 'emit'); }) test('FrameElementInit constructor : empty path', () => { @@ -93,7 +101,14 @@ describe('test frame elements', () => { name: `${FRAME_ELEMENT}:CARD_NUMBER:123:ERROR:`, location: { href: `http://localhost/?${btoa(JSON.stringify({record:element, metaData: {clientDomain: 'https://demo.com'}}))}`, - } + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), })) const onSpy = jest.spyOn(bus, 'on'); FrameElementInit.startFrameElement('123') @@ -155,7 +170,14 @@ describe('test composable frame elements', () => { name: `${FRAME_ELEMENT}:group:${btoa('123')}:ERROR:`, location: { href: `http://localhost/?${btoa(JSON.stringify({record:element, metaData: {clientDomain: 'https://demo.com'}}))}`, - } + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), })); emitSpy = jest.spyOn(bus, 'emit'); @@ -166,7 +188,14 @@ describe('test composable frame elements', () => { name: `${FRAME_ELEMENT}:CARD_NUMBER:${btoa('123')}:ERROR:`, location: { href: `http://localhost/?${btoa(JSON.stringify({record:element, metaData: {clientDomain: 'https://demo.com'}}))}`, - } + }, + parent: { + postMessage: (message, targetOrigin, ...args) => { + if (!targetOrigin) targetOrigin = "*"; + // Optionally, call a jest mock here + } + }, + addEventListener: jest.fn(), })) const onSpy = jest.spyOn(bus, 'on'); FrameElementInit.group = []; diff --git a/tests/core/internal/iframe-form/iframe-form.test.js b/tests/core/internal/iframe-form/iframe-form.test.js index 0862c049..3c58e88b 100644 --- a/tests/core/internal/iframe-form/iframe-form.test.js +++ b/tests/core/internal/iframe-form/iframe-form.test.js @@ -67,14 +67,17 @@ describe('test iframeFormelement', () => { targetSpy.mockReturnValue({ on, }); - windowSpy = jest.spyOn(window,'parent','get'); + windowSpy = jest.spyOn(global, 'window', 'get'); windowSpy.mockImplementation(()=>({ - - frames:{ + parent:{ + frames:{ 'element:CARD_NUMBER:${tableCol}':{document:{ getElementById:()=>({value:testValue}) }} - } + }, + postMessage: jest.fn(), + }, + addEventListener: jest.fn(), })); }); afterEach(() => { @@ -2202,58 +2205,6 @@ describe('test file Upload method', () => { }); }); - // test.only('initialize iFrame and upload with file input', () => { - // // const form = new IFrameForm("controllerId", "", "ERROR"); - // // form.setClient(fileClientObj) - // // form.setClientMetadata(metaData) - // // form.setContext(context) - - // // const frameReadyEvent = on.mock.calls.filter((data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.FRAME_READY + 'controllerId'); - // // const frameReadyCb = frameReadyEvent[0][1]; - - // // expect(() => { frameReadyCb({}) }).toThrow(SkyflowError) - - // // frameReadyCb({ name: COLLECT_FRAME_CONTROLLER }) - - // // expect(() => { frameReadyCb({ name: "element:type:aW52YWxpZA==" }) }).toThrow(SkyflowError) - // const skyflowInit = jest.fn(); - // windowSpy = jest.spyOn(global, 'window', 'get'); - // windowSpy.mockImplementation(() => ({ - // name: `${FRAME_ELEMENT}:FILE_INPUT:123:ERROR:`, - // location: { - // href: `http://localhost/?${btoa(JSON.stringify({record: element, metaData: {clientDomain: 'http://localhost.com'}}))}`, - // } - // })) - - // // frameReadyCb({ name: file_element }) - - // const createFormElement = skyflowInit.mock.calls[0][0] - // const fileElement = createFormElement(file_element) - - // const fileUploadEvent = on.mock.calls.filter((data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.FILE_UPLOAD + 'controllerId'); - // const fileUploadCb = fileUploadEvent[0][1]; - // const cb2 = jest.fn(); - // fileUploadCb(fileData, cb2) - - // setTimeout(() => { - // expect(cb2.mock.calls[0][0].error).toBeDefined() - // }, 3000) - - // setTimeout(() => { - // expect(cb2.mock.calls[0][0].error).toBeDefined() - // }, 3000) - - // fileElement.setValue({ - // lastModified: '', - // lastModifiedDate: '', - // name: "sample.jpg", - // size: 48848, - // type: "image/jpeg", - // webkitRelativePath: "" - // }) - // const cb3 = jest.fn() - // fileUploadCb(fileData, cb3) - // }); test('validate for file input - valid blockZeroSizeFile boolean', () => { const elementType = ELEMENTS.FILE_INPUT.name; var options = { blockEmptyFiles: true}; diff --git a/tests/core/internal/internal-index.test.js b/tests/core/internal/internal-index.test.js index a36f4a33..ce646dc5 100644 --- a/tests/core/internal/internal-index.test.js +++ b/tests/core/internal/internal-index.test.js @@ -42,7 +42,13 @@ describe('domReady function - FrameElement', () => { on, }); windowSpy = jest.spyOn(window,'parent','get'); - + const originalPostMessage = window.postMessage; + window.postMessage = (message, targetOrigin, ...args) => { + if (!targetOrigin) { + targetOrigin = "*"; + } + return originalPostMessage.call(window, message, targetOrigin, ...args); + }; mockIFrameFormElement = { resetEvents: jest.fn(), on: jest.fn(), diff --git a/tests/core/internal/reveal/reveal-frame.test.js b/tests/core/internal/reveal/reveal-frame.test.js index e9c0e2ff..31f006c7 100644 --- a/tests/core/internal/reveal/reveal-frame.test.js +++ b/tests/core/internal/reveal/reveal-frame.test.js @@ -55,6 +55,20 @@ const defineUrl = (url) => { value: "reveal:1234", writable: true, }); + Object.defineProperty(window, "parent", { + value: { + frames: { + "element:CARD_NUMBER:${tableCol}": { + document: { + getElementById: () => ({ value: testValue }), + }, + }, + }, + postMessage: jest.fn(), + addEventListener: jest.fn(), + }, + writable: true, + }); }; const elementName = "reveal:1234" @@ -171,10 +185,10 @@ name: elementName, // // reveal response ready - const onRevealResponseName = on.mock.calls[0][0]; + const onRevealResponseName = on.mock.calls[1][0]; // undefined since with jest window.name will be emptyString("") expect(onRevealResponseName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY); - const onRevealResponseCb = on.mock.calls[0][1]; + const onRevealResponseCb = on.mock.calls[1][1]; onRevealResponseCb({"1815-6223-1073-1425":"card_value"}) }); @@ -213,10 +227,10 @@ name: elementName, // reveal response ready - const onRevealResponseName = on.mock.calls[0][0]; + const onRevealResponseName = on.mock.calls[1][0]; // undefined since with jest window.name will be emptyString("") expect(onRevealResponseName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY); - const onRevealResponseCb = on.mock.calls[0][1]; + const onRevealResponseCb = on.mock.calls[1][1]; onRevealResponseCb({"1815":"1234"}) }); test("init callback after reveal without value",()=>{ @@ -257,10 +271,10 @@ name: elementName, // reveal response ready - const onRevealResponseName = on.mock.calls[0][0]; + const onRevealResponseName = on.mock.calls[1][0]; // undefined since with jest window.name will be emptyString("") expect(onRevealResponseName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY); - const onRevealResponseCb = on.mock.calls[0][1]; + const onRevealResponseCb = on.mock.calls[1][1]; onRevealResponseCb({}); }); @@ -305,10 +319,10 @@ name: elementName, expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName); - const onSetErrorName = on.mock.calls[1][0]; + const onSetErrorName = on.mock.calls[2][0]; // undefined since with jest window.name will be emptyString("") expect(onSetErrorName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR+ elementName); - const onSetErrorCb = on.mock.calls[1][1]; + const onSetErrorCb = on.mock.calls[2][1]; onSetErrorCb({ name:elementName, isTriggerError: true, @@ -355,10 +369,10 @@ name: elementName, // reveal response ready - const onRevealResponseName = on.mock.calls[1][0]; + const onRevealResponseName = on.mock.calls[2][0]; // undefined since with jest window.name will be emptyString("") expect(onRevealResponseName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR+ elementName);; - const onRevealResponseCb = on.mock.calls[1][1]; + const onRevealResponseCb = on.mock.calls[2][1]; onRevealResponseCb({ name: elementName, isTriggerError: false, @@ -401,10 +415,10 @@ name: elementName, const emitCb = emitSpy.mock.calls[0][2]; expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const onRevealResponseName = on.mock.calls[2][0]; + const onRevealResponseName = on.mock.calls[3][0]; // undefined since with jest window.name will be emptyString("") expect(onRevealResponseName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS+ elementName); - const onRevealResponseCb = on.mock.calls[2][1]; + const onRevealResponseCb = on.mock.calls[3][1]; onRevealResponseCb({ name: elementName, updateType:REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN, @@ -449,10 +463,10 @@ name: elementName, expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const onRevealResponseName = on.mock.calls[2][0]; + const onRevealResponseName = on.mock.calls[3][0]; // undefined since with jest window.name will be emptyString("") expect(onRevealResponseName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS+ elementName); - const onRevealResponseCb = on.mock.calls[2][1]; + const onRevealResponseCb = on.mock.calls[3][1]; onRevealResponseCb({ name: elementName, updateType:REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, @@ -497,10 +511,10 @@ name: elementName, expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const onRevealResponseName = on.mock.calls[2][0]; + const onRevealResponseName = on.mock.calls[3][0]; // undefined since with jest window.name will be emptyString("") expect(onRevealResponseName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS+ elementName); - const onRevealResponseCb = on.mock.calls[2][1]; + const onRevealResponseCb = on.mock.calls[3][1]; onRevealResponseCb({ name: elementName, updateType:REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, @@ -671,9 +685,9 @@ name: elementName, expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const onRevealResponseName = on.mock.calls[2][0]; + const onRevealResponseName = on.mock.calls[3][0]; expect(onRevealResponseName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS+ elementName); - const onRevealResponseCb = on.mock.calls[2][1]; + const onRevealResponseCb = on.mock.calls[3][1]; onRevealResponseCb({ name: elementName, updateType:REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, @@ -712,9 +726,9 @@ name: elementName, expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const eventRenderResponse = on.mock.calls[3][0]; + const eventRenderResponse = on.mock.calls[0][0]; expect(eventRenderResponse).toBe(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY+ elementName); - const callback = on.mock.calls[3][1]; + const callback = on.mock.calls[0][1]; callback( { url: "https://fileurl?response-content-disposition=inline%3B%20filename%3Ddummylicence.png&X-Amz-Signature=4a19c53917cc21df2bd05bc28e4e316ffc36c208d005d8f3f50631", iframeName: elementName, @@ -764,9 +778,9 @@ name: elementName, expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const eventRenderResponse = on.mock.calls[3][0]; + const eventRenderResponse = on.mock.calls[0][0]; expect(eventRenderResponse).toBe(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY+ elementName); - const callback = on.mock.calls[3][1]; + const callback = on.mock.calls[0][1]; callback({ url: "https://url?response-content-disposition=inline%3B%20filename%3Ddummylicence.pdf&X-Amz-Signature=4a19c53917cc21df2bd05bc28e4e316ffc36c208d005d8f3f50631", iframeName: elementName, @@ -789,9 +803,9 @@ name: elementName, expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const eventRenderResponse = on.mock.calls[3][0]; + const eventRenderResponse = on.mock.calls[0][0]; expect(eventRenderResponse).toBe(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY+ elementName); - const callback = on.mock.calls[3][1]; + const callback = on.mock.calls[0][1]; callback({ url: "https://fileurl?response-content-disposition=inline%3B%20filename%3Ddummylicence.png&X-Amz-Signature=4a19c53917cc21df2bd05bc28e4e316ffc36c208d005d8f3f50631", iframeName: elementName, @@ -829,9 +843,9 @@ name: elementName, expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const eventRenderResponse = on.mock.calls[3][0]; + const eventRenderResponse = on.mock.calls[0][0]; expect(eventRenderResponse).toBe(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY+ elementName); - const callback = on.mock.calls[3][1]; + const callback = on.mock.calls[0][1]; callback({ url: "https://fileurl?filename%3Ddummylicence.pdf&X-Amz-Signature=4a19c53917cc21df2bd05bc28e4e316ffc36c208d005d8f3f50631", iframeName: elementName, @@ -870,9 +884,9 @@ name: elementName, expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const eventRenderResponse = on.mock.calls[3][0]; + const eventRenderResponse = on.mock.calls[0][0]; expect(eventRenderResponse).toBe(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY+ elementName); - const callback = on.mock.calls[3][1]; + const callback = on.mock.calls[0][1]; callback({ error: DEFAULT_FILE_RENDER_ERROR, iframeName: elementName, @@ -993,9 +1007,9 @@ describe("Reveal Frame Class", () => { expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; console.log('======>>>>>>>>>', emitSpy.mock.calls, onMock.mock.calls); // Verify reveal response ready - const onRevealResponseName = onMock.mock.calls[0][0]; + const onRevealResponseName = onMock.mock.calls[1][0]; expect(onRevealResponseName).toBe(ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY); - const onRevealResponseCb = onMock.mock.calls[0][1]; + const onRevealResponseCb = onMock.mock.calls[1][1]; onRevealResponseCb({ "1815-6223-1073-1425": "card_value" }); }); @@ -1028,9 +1042,9 @@ describe("Reveal Frame Class", () => { const emittedEventName = emitSpy.mock.calls[0][0]; const emitCb = emitSpy.mock.calls[0][2]; expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; - const eventRenderResponse = onMock.mock.calls[3][0]; + const eventRenderResponse = onMock.mock.calls[0][0]; expect(eventRenderResponse).toBe(ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + elementName); - const callback = onMock.mock.calls[3][1]; + const callback = onMock.mock.calls[0][1]; callback({ error: DEFAULT_FILE_RENDER_ERROR, iframeName: elementName, @@ -1073,7 +1087,6 @@ describe("Reveal Frame Class", () => { defineUrl("http://localhost/?" + btoa(JSON.stringify(data))); const testFrame = RevealFrame.init(); const emittedEventName = emitSpy.mock.calls[0][0]; - console.log("testFrame===>", emitSpy.mock.calls); const emittedData = emitSpy.mock.calls[0][1]; expect(emittedEventName).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED+ elementName);; expect(emittedData).toEqual({name : elementName}) diff --git a/tests/skyflow.test.js b/tests/skyflow.test.js index 92bf6b61..e19db09a 100644 --- a/tests/skyflow.test.js +++ b/tests/skyflow.test.js @@ -12,6 +12,7 @@ import ComposableContainer from '../src/core/external/collect/compose-collect-co import SkyflowContainer from '../src/core/external/skyflow-container'; import Client from '../src/client' import logs from '../src/utils/logs'; +import { ComposableRevealContainer } from '../src/index-node'; jest.mock('../src/utils/jwt-utils', () => ({ __esModule: true, @@ -90,6 +91,9 @@ describe('Create container', () => { const composableContainer = skyflow.container(ContainerType.COMPOSABLE,{layout:[1]}); expect(composableContainer).toBeInstanceOf(ComposableContainer); + const revealComposableContainer = skyflow.container(ContainerType.COMPOSE_REVEAL,{layout:[1]}); + expect(revealComposableContainer).toBeInstanceOf(ComposableRevealContainer); + try { const revealContainer = skyflow.container('test'); } catch (err) { diff --git a/tests/utils/jwt-utils.test.js b/tests/utils/jwt-utils.test.js index e6c4f52e..332e11c5 100644 --- a/tests/utils/jwt-utils.test.js +++ b/tests/utils/jwt-utils.test.js @@ -5,7 +5,7 @@ import isTokenValid from "../../src/utils/jwt-utils"; jest.mock('jwt-decode', () => () => ({exp: 123})) describe('Validation token', () => { - + test('empty token', () => { const res = isTokenValid("") expect(res).toBe(false) @@ -20,4 +20,4 @@ describe('Validation token', () => { const res = isTokenValid("token") expect(res).toBe(false) }) -}); \ No newline at end of file +});