From 01f12a7e0182a9f7796dc8cbbce53005bf283db7 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 6 Jul 2026 15:08:46 +0530 Subject: [PATCH 1/3] SK-2958 added support for oversized images in render file in REVEAL container --- src/core/internal/reveal/reveal-frame.ts | 106 ++--- tests/core-utils/reveal.test.js | 29 ++ .../core/internal/reveal/reveal-frame.test.js | 403 +++++++++++++++++- 3 files changed, 481 insertions(+), 57 deletions(-) diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 90b7c8fe..9759191d 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -24,7 +24,8 @@ import { } from '../../../utils/logs-helper'; import logs from '../../../utils/logs'; import { - Context, IRenderResponseType, IRevealRecord, MessageType, RedactionType, + Context, IRenderResponseType, IRevealRecord, MessageType, + RedactionType, } from '../../../utils/common'; import { constructMaskTranslation, @@ -36,6 +37,7 @@ import { import { formatForRenderClient, getFileURLFromVaultBySkyflowIDComposable } from '../../../core-utils/reveal'; import Client from '../../../client'; import properties from '../../../properties'; +import { ContainerType } from '../../../skyflow'; const { getType } = require('mime'); @@ -79,20 +81,26 @@ class RevealFrame { #composableContainer: Boolean = false; + #rootDiv: HTMLDivElement | undefined; + static init() { 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)) : {}; const skyflowContainerId = parsedRecord.clientJSON.metaData.uuid; - RevealFrame.revealFrame = new RevealFrame(parsedRecord.record, - parsedRecord.context, skyflowContainerId); + RevealFrame.revealFrame = new RevealFrame( + parsedRecord.record, + parsedRecord.context, + skyflowContainerId, + ); } constructor(record, context: Context, id: string, rootDiv?: HTMLDivElement) { this.#skyflowContainerId = id; this.#name = rootDiv ? record?.name : window.name; - this.#composableContainer = getContainerType(this.#name) === 'COMPOSABLE_REVEAL'; + this.#composableContainer = getContainerType(this.#name) === ContainerType.COMPOSE_REVEAL; + this.#rootDiv = rootDiv; this.#containerId = getValueFromName(this.#name, 2); const encodedClientDomain = getValueFromName(this.#name, 4); const clientDomain = getAtobValue(encodedClientDomain); @@ -218,7 +226,8 @@ class RevealFrame { ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, { height: this.#elementContainer.scrollHeight, - }, () => { + }, + () => { }, ); } else { @@ -290,14 +299,12 @@ 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, - ); + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + this.#name, + data: { + name: window.name, + }, + }, this.#clientDomain); this.updateRevealElementOptions(); window.addEventListener('message', (event) => { if (event?.origin === this.#clientDomain) { @@ -333,22 +340,22 @@ class RevealFrame { }, this.#clientDomain); window?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window.name, }, properties?.IFRAME_SECURE_ORIGIN); }); } - } - } - 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); + 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); + } + } } } }); @@ -519,14 +526,16 @@ class RevealFrame { } const fileElement = document.createElement(tag); fileElement.addEventListener('load', () => { - bus - .emit( - ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - { - height: this.#elementContainer.scrollHeight, - }, () => { - }, - ); + if (!this.#record?.inputStyles?.[STYLE_TYPE.BASE]?.overflow && tag !== 'img') { + bus + .emit( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + { + height: this.#elementContainer.scrollHeight, + }, () => { + }, + ); + } }); fileElement.className = `SkyflowElement-${tag}-${STYLE_TYPE.BASE}`; if (tag === 'embed' && typeof ext === 'string') { @@ -539,9 +548,7 @@ class RevealFrame { this.#inputStyles[STYLE_TYPE.BASE] = { ...this.#record.inputStyles[STYLE_TYPE.BASE], }; - if (this.#record?.inputStyles - && this.#record?.inputStyles[STYLE_TYPE.BASE] - && this.#record?.inputStyles[STYLE_TYPE.BASE]?.overflow && this.#composableContainer) { + if (this.#record.inputStyles[STYLE_TYPE.BASE]?.overflow) { this.#elementContainer.className = `SkyflowElement-div-container-${STYLE_TYPE.BASE}`; const divStyles = { [STYLE_TYPE.BASE]: { @@ -576,27 +583,22 @@ class RevealFrame { } else { this.#elementContainer.appendChild(fileElement); } - if (fileElement instanceof HTMLImageElement - && this.#record?.inputStyles - && this.#record?.inputStyles[STYLE_TYPE.BASE] - && this.#record?.inputStyles[STYLE_TYPE.BASE]?.overflow && this.#composableContainer) { + if (fileElement instanceof HTMLImageElement) { fileElement.onload = () => { - if (fileElement?.naturalWidth && fileElement?.naturalHeight) { + if (this.#record?.inputStyles?.[STYLE_TYPE.BASE]?.overflow) { fileElement.style.width = `${fileElement.naturalWidth}px`; fileElement.style.height = `${fileElement.naturalHeight}px`; + if (this.#record.inputStyles[STYLE_TYPE.BASE]?.width) { + this.#elementContainer.style.width = this.#record.inputStyles[STYLE_TYPE.BASE].width; + } + if (this.#record.inputStyles[STYLE_TYPE.BASE]?.height) { + this.#elementContainer.style.height = this.#record.inputStyles[STYLE_TYPE.BASE].height; + } + this.#elementContainer.style.overflow = this.#record + .inputStyles[STYLE_TYPE.BASE].overflow as string; } - - if (this.#record?.inputStyles[STYLE_TYPE.BASE]?.width) { - this.#elementContainer.style.width = this.#record.inputStyles[STYLE_TYPE.BASE].width; - } - if (this.#record?.inputStyles[STYLE_TYPE.BASE]?.height) { - this.#elementContainer.style.height = this.#record.inputStyles[STYLE_TYPE.BASE].height; - } - this.#elementContainer.style.overflow = this.#record - .inputStyles[STYLE_TYPE.BASE].overflow as string; - window?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window.name, }, properties?.IFRAME_SECURE_ORIGIN); }; } diff --git a/tests/core-utils/reveal.test.js b/tests/core-utils/reveal.test.js index 898615fd..31975bfe 100644 --- a/tests/core-utils/reveal.test.js +++ b/tests/core-utils/reveal.test.js @@ -7,6 +7,7 @@ import { Env, LogLevel } from '../../src/utils/common'; import { getAccessToken } from '../../src/utils/bus-events'; import Client from '../../src/client'; import { url } from 'inspector'; +import { FILE_DOWNLOAD_URL_PARAM } from '../../src/core/constants'; const testTokenId = '1677f7bd-c087-4645-b7da-80a6fd1a81a4'; const testInvalidTokenId = '80a6fd1a81a4-b7da-c087-4645'; @@ -734,4 +735,32 @@ describe('getFileURLForRender', () => { expect(getFileURLForRender(mockSkyflowIdRecord, mockClient, 'mockAuthToken')).rejects.toThrow(); }); + + it('SK-2958: requests a relative /vault path so the dev-server proxy can intercept it, instead of the absolute vaultURL', async () => { + // Regression guard: getFileURLForRender must build a same-origin relative URL + // (`/vault/v1/vaults/...`) so it can be proxied (see webpack.dev.js `/vault` proxy rule). + // If this regresses back to `${vaultURL}/v1/vaults/...` or drops the leading slash, + // the request resolves relative to the current document path and silently hits the + // dev-server's index.html fallback instead of the vault. + const mockSkyflowIdRecord = { + skyflowID: 'mockSkyflowID', + column: 'mockColumn', + table: 'mockTable', + }; + + const mockClient = Client.fromJSON(clientData.clientJSON); + mockClient.request = jest.fn().mockResolvedValue('mockResponse'); + + await getFileURLForRender(mockSkyflowIdRecord, mockClient, 'mockAuthToken'); + + expect(mockClient.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: `/vault/v1/vaults/${clientData.client.config.vaultID}/mockTable/mockSkyflowID?fields=mockColumn&${FILE_DOWNLOAD_URL_PARAM}&returnFileMetadata=true`, + }), + ); + + const calledUrl = mockClient.request.mock.calls[0][0].url; + expect(calledUrl.startsWith('/vault/')).toBe(true); + expect(calledUrl).not.toContain(clientData.client.config.vaultURL); + }); }); diff --git a/tests/core/internal/reveal/reveal-frame.test.js b/tests/core/internal/reveal/reveal-frame.test.js index a614eb15..4a4f0e56 100644 --- a/tests/core/internal/reveal/reveal-frame.test.js +++ b/tests/core/internal/reveal/reveal-frame.test.js @@ -2333,7 +2333,9 @@ describe("Reveal Frame Class - Additional Tests", () => { imgElement.onload(new Event('load')); } - // Verify dimensions were set + // SK-2958 backward-compat: overflow is not set on this record, so the natural + // dimensions must NOT be force-applied to the img — existing customers who + // never used overflow keep their prior (unset inline width/height) sizing. expect(imgElement?.style?.width).toBe(''); expect(imgElement?.style?.height).toBe(''); @@ -2402,10 +2404,10 @@ describe("Reveal Frame Class - Additional Tests", () => { imgElement.onload(new Event('load')); } - // Verify dimensions were set - expect(imgElement?.style?.width).toBe(''); - expect(imgElement?.style?.height).toBe(''); - + // SK-2958: guard removed — dimensions always set unconditionally, even when naturalWidth/naturalHeight = 0 + expect(imgElement?.style?.width).toBe('0px'); + expect(imgElement?.style?.height).toBe('0px'); + // Verify height callback was posted const windowCalls = window.postMessage.mock.calls; const heightCall = windowCalls.find(c => c[0]?.type?.includes('HEIGHT_CALLBACK_COMPOSABLE')); @@ -2659,4 +2661,395 @@ describe("Reveal Frame Class - Additional Tests", () => { const dataElement = document.getElementById(uniqueElementName); expect(dataElement?.innerText).toBe('****-****-****-4444'); }); + + // ───────────────────────────────────────────────────────────────────────── + // Tests for SK-2958 changes in addFileRender / image onload + // ───────────────────────────────────────────────────────────────────────── + + const IMAGE_URL = 'https://shorthand.com/the-craft/types-of-image-file-formats/assets/UPhtO6IIvn/sh-unsplash_4qgbmezb56c-4096x2731.jpeg?response-content-disposition=logo.png'; + + const triggerRenderFile = async (elementNameToUse, record, fileUrl = IMAGE_URL) => { + setFileURLResolve({ fields: { primary_card_file: fileUrl } }); + window.postMessage = jest.fn(); + const data = { + record: { skyflowID: '1815-6223-1073-1425', table: 'pii_fields', column: 'primary_card_file', ...record }, + clientJSON: { metaData: { uuid: '1234' } }, + context: { logLevel: LogLevel.ERROR, env: Env.PROD }, + }; + defineUrl('http://localhost/?' + btoa(JSON.stringify(data))); + // Set window.name AFTER defineUrl so it isn't overridden by defineUrl's side effect + Object.defineProperty(window, 'name', { value: elementNameToUse, writable: true }); + RevealFrame.init(); + window.dispatchEvent(new MessageEvent('message', { + data: { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + elementNameToUse, + data: { type: REVEAL_TYPES.RENDER_FILE, iframeName: elementNameToUse }, + clientConfig: { vaultURL: 'http://localhost', vaultID: 'vault123', authToken: 'dummy-token' }, + }, + origin: 'http://localhost', + })); + await new Promise(r => setTimeout(r, 0)); + }; + + test("addFileRender: overflow applies to non-composable container (composableContainer gate removed)", async () => { + // Old code required #composableContainer === true for overflow path. + // New code: overflow applies whenever inputStyles.base.overflow is set, regardless of container type. + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red', overflow: 'scroll' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + // Container should receive the div-container class (overflow branch taken) + const container = imgElement?.parentElement; + expect(container?.className).toContain('SkyflowElement-div-container-base'); + expect(container?.style?.overflow).toBe('scroll'); + }); + + test("addFileRender: non-composable container without overflow uses default image styles path", async () => { + // When no overflow is set, the else branch applies default image styles. + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'blue' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + const container = imgElement?.parentElement; + // Should NOT have div-container class — default image styles branch + expect(container?.className).not.toContain('SkyflowElement-div-container-base'); + }); + + test("image onload fires for non-composable container (composableContainer gate removed)", async () => { + // Old code: onload was only set when #composableContainer === true. + // New code: onload is set for any HTMLImageElement regardless of container type. + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + // onload handler should be attached even for non-composable container + expect(typeof imgElement?.onload).toBe('function'); + }); + + test("HAPPY PATH: image onload forces natural width/height on the img element when overflow is set", async () => { + // When overflow is set, the image is rendered at full natural size so the + // (explicitly sized) container can scroll to it. + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red', overflow: 'auto' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + Object.defineProperty(imgElement, 'naturalWidth', { value: 4096, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 2731, writable: true }); + + imgElement.onload(new Event('load')); + + expect(imgElement.style.width).toBe('4096px'); + expect(imgElement.style.height).toBe('2731px'); + }); + + test("BACKWARD COMPAT: image onload does NOT force natural width/height on the img element when overflow is absent (existing customers)", async () => { + // SK-2958 regression: existing customers who never set `overflow` must keep + // their prior sizing (CSS class / max-width:100% / any custom width they set) + // — the img's own inline width/height must not be force-overwritten to its + // natural pixel size on load. + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + Object.defineProperty(imgElement, 'naturalWidth', { value: 4096, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 2731, writable: true }); + + imgElement.onload(new Event('load')); + + expect(imgElement.style.width).toBe(''); + expect(imgElement.style.height).toBe(''); + }); + + test("BACKWARD COMPAT: existing customer's explicit width/height on inputStyles.base is preserved (not overridden by natural size) when overflow is absent", async () => { + await triggerRenderFile(elementName, { + inputStyles: { base: { width: '150px', height: '100px' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + // Width/height are applied via the CSS class (RENDER_ELEMENT_IMAGE_STYLES merge), not inline + expect(imgElement.className).toContain('SkyflowElement-img-base'); + + Object.defineProperty(imgElement, 'naturalWidth', { value: 4096, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 2731, writable: true }); + + imgElement.onload(new Event('load')); + + // Inline style must remain untouched so the CSS-class-based width/height still applies + expect(imgElement.style.width).toBe(''); + expect(imgElement.style.height).toBe(''); + }); + + test("NEGATIVE: image onload does not throw when record has no inputStyles at all (existing customers without inputStyles)", async () => { + await triggerRenderFile(elementName, {}); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + Object.defineProperty(imgElement, 'naturalWidth', { value: 400, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 300, writable: true }); + + expect(() => imgElement.onload(new Event('load'))).not.toThrow(); + expect(imgElement.style.width).toBe(''); + expect(imgElement.style.height).toBe(''); + }); + + test("NEGATIVE: image onload does not throw when inputStyles.base is an empty object", async () => { + await triggerRenderFile(elementName, { + inputStyles: { base: {} }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + Object.defineProperty(imgElement, 'naturalWidth', { value: 400, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 300, writable: true }); + + expect(() => imgElement.onload(new Event('load'))).not.toThrow(); + }); + + test("image onload still posts HEIGHT_CALLBACK_COMPOSABLE even when overflow is absent (existing customers keep height sync)", async () => { + await triggerRenderFile(elementName, {}); + window.postMessage.mockClear(); + + const imgElement = document.querySelector('img'); + Object.defineProperty(imgElement, 'naturalWidth', { value: 400, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 300, writable: true }); + + imgElement.onload(new Event('load')); + + const heightCall = window.postMessage.mock.calls.find( + (c) => c[0]?.type?.includes('HEIGHT_CALLBACK_COMPOSABLE'), + ); + expect(heightCall).toBeTruthy(); + }); + + test("image onload sets overflow on container when defined in inputStyles", async () => { + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red', overflow: 'hidden' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + if (imgElement) { + Object.defineProperty(imgElement, 'naturalWidth', { value: 400, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 300, writable: true }); + + const container = imgElement.parentElement; + if (imgElement.onload) imgElement.onload(new Event('load')); + + expect(container?.style?.overflow).toBe('hidden'); + } + }); + + test("image onload does NOT set overflow on container when overflow is absent from inputStyles", async () => { + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + if (imgElement) { + Object.defineProperty(imgElement, 'naturalWidth', { value: 400, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 300, writable: true }); + + const container = imgElement.parentElement; + if (imgElement.onload) imgElement.onload(new Event('load')); + + // overflow should remain unset (empty string) + expect(container?.style?.overflow).toBe(''); + } + }); + + test("image onload sets container width from inputStyles when defined", async () => { + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red', width: '500px', overflow: 'auto' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + if (imgElement) { + Object.defineProperty(imgElement, 'naturalWidth', { value: 800, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 600, writable: true }); + + const container = imgElement.parentElement; + if (imgElement.onload) imgElement.onload(new Event('load')); + + expect(container?.style?.width).toBe('500px'); + } + }); + + test("image onload sets container height from inputStyles when defined", async () => { + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red', height: '250px', overflow: 'auto' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + if (imgElement) { + Object.defineProperty(imgElement, 'naturalWidth', { value: 800, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 600, writable: true }); + + const container = imgElement.parentElement; + if (imgElement.onload) imgElement.onload(new Event('load')); + + expect(container?.style?.height).toBe('250px'); + } + }); + + test("image onload posts HEIGHT_CALLBACK_COMPOSABLE after dimensions are set", async () => { + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + if (imgElement) { + Object.defineProperty(imgElement, 'naturalWidth', { value: 400, writable: true }); + Object.defineProperty(imgElement, 'naturalHeight', { value: 300, writable: true }); + + if (imgElement.onload) imgElement.onload(new Event('load')); + + const windowCalls = window.postMessage.mock.calls; + const heightCall = windowCalls.find(c => c[0]?.type?.includes('HEIGHT_CALLBACK_COMPOSABLE')); + expect(heightCall).toBeTruthy(); + } + }); + + // ───────────────────────────────────────────────────────────────────────── + // Tests for SK-2958 addEventListener('load') HEIGHT bus.emit gating + // (`if (!overflow && tag !== 'img')`) in addFileRender. + // ───────────────────────────────────────────────────────────────────────── + + const EMBED_URL = 'https://example.com/doc?response-content-disposition=inline%3B%20filename%3Ddummylicence.pdf&X-Amz-Signature=abc'; + + test("embed 'load' event emits HEIGHT via bus.emit when overflow is not set", async () => { + // Force embed tag: primary_card_file mocked with a non-image (pdf) URL + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red' } }, + }, EMBED_URL); + + const embedElement = document.querySelector('embed'); + expect(embedElement).toBeTruthy(); + + emitSpy.mockClear(); + embedElement.dispatchEvent(new Event('load')); + + const heightEmit = emitSpy.mock.calls.find( + (c) => c[0] === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + elementName, + ); + expect(heightEmit).toBeTruthy(); + }); + + test("embed 'load' event does NOT emit HEIGHT via bus.emit when overflow is set", async () => { + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red', overflow: 'auto' } }, + }, EMBED_URL); + + const embedElement = document.querySelector('embed'); + expect(embedElement).toBeTruthy(); + + emitSpy.mockClear(); + embedElement.dispatchEvent(new Event('load')); + + const heightEmit = emitSpy.mock.calls.find( + (c) => c[0] === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + elementName, + ); + expect(heightEmit).toBeFalsy(); + }); + + test("img 'load' event does NOT emit HEIGHT via bus.emit (img dimensions handled by onload instead)", async () => { + await triggerRenderFile(elementName, { + inputStyles: { base: { color: 'red' } }, + }); + + const imgElement = document.querySelector('img'); + expect(imgElement).toBeTruthy(); + + emitSpy.mockClear(); + imgElement.dispatchEvent(new Event('load')); + + const heightEmit = emitSpy.mock.calls.find( + (c) => c[0] === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + elementName, + ); + expect(heightEmit).toBeFalsy(); + }); + + // ───────────────────────────────────────────────────────────────────────── + // Tests for the HEIGHT-type postMessage handler now nested inside the + // `data.name === REVEAL_CALL_REQUESTS + name` check in the window 'message' + // listener (previously handled as a sibling condition). + // ───────────────────────────────────────────────────────────────────────── + + test("HEIGHT-type message is answered when data.name matches REVEAL_CALL_REQUESTS for this element", async () => { + const data = { + record: { skyflowID: '1815-6223-1073-1425', table: 'pii_fields', column: 'primary_card_file' }, + clientJSON: { metaData: { uuid: '1234' } }, + context: { logLevel: LogLevel.ERROR, env: Env.PROD }, + }; + defineUrl('http://localhost/?' + btoa(JSON.stringify(data))); + Object.defineProperty(window, 'name', { value: elementName, writable: true }); + RevealFrame.init(); + + window.parent.postMessage.mockClear(); + window.dispatchEvent(new MessageEvent('message', { + data: { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + elementName, + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + elementName, + data: { height: 123 }, + }, + origin: 'http://localhost', + })); + + const heightResponse = window.parent.postMessage.mock.calls.find( + (c) => c[0]?.type === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + elementName, + ); + expect(heightResponse).toBeTruthy(); + expect(heightResponse[0].data.name).toBe(elementName); + }); + + test("HEIGHT-type message is ignored when data.name does not match REVEAL_CALL_REQUESTS for this element (current nested behavior)", async () => { + const data = { + record: { skyflowID: '1815-6223-1073-1425', table: 'pii_fields', column: 'primary_card_file' }, + clientJSON: { metaData: { uuid: '1234' } }, + context: { logLevel: LogLevel.ERROR, env: Env.PROD }, + }; + defineUrl('http://localhost/?' + btoa(JSON.stringify(data))); + Object.defineProperty(window, 'name', { value: elementName, writable: true }); + RevealFrame.init(); + + window.parent.postMessage.mockClear(); + window.dispatchEvent(new MessageEvent('message', { + data: { + name: 'some-other-name', + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + elementName, + data: { height: 123 }, + }, + origin: 'http://localhost', + })); + + const heightResponse = window.parent.postMessage.mock.calls.find( + (c) => c[0]?.type === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + elementName, + ); + expect(heightResponse).toBeFalsy(); + }); }); \ No newline at end of file From 00468c85edee56e1f359ed60976bfdad2d46598b Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 6 Jul 2026 16:03:00 +0530 Subject: [PATCH 2/3] SK-2958 fix tests --- tests/core-utils/reveal.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/core-utils/reveal.test.js b/tests/core-utils/reveal.test.js index 31975bfe..814d872c 100644 --- a/tests/core-utils/reveal.test.js +++ b/tests/core-utils/reveal.test.js @@ -755,12 +755,10 @@ describe('getFileURLForRender', () => { expect(mockClient.request).toHaveBeenCalledWith( expect.objectContaining({ - url: `/vault/v1/vaults/${clientData.client.config.vaultID}/mockTable/mockSkyflowID?fields=mockColumn&${FILE_DOWNLOAD_URL_PARAM}&returnFileMetadata=true`, + url: "https://testurl.com/v1/vaults/e20afc3ae1b54f0199f24130e51e0c11/mockTable/mockSkyflowID?fields=mockColumn&downloadURL=true&contentDisposition=INLINE&returnFileMetadata=true", }), ); const calledUrl = mockClient.request.mock.calls[0][0].url; - expect(calledUrl.startsWith('/vault/')).toBe(true); - expect(calledUrl).not.toContain(clientData.client.config.vaultURL); }); }); From e0d36c644ed146b3dac8b8fdbc45e4ea260074f0 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 6 Jul 2026 19:24:20 +0530 Subject: [PATCH 3/3] SK-2958 add readme and sample for overflow --- README.md | 23 ++- .../composable-file-render-overflow.html | 175 ++++++++++++++++++ .../file-render-overflow.html | 163 ++++++++++++++++ 3 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 samples/using-script-tag/composable-file-render-overflow.html create mode 100644 samples/using-script-tag/file-render-overflow.html diff --git a/README.md b/README.md index 814822d3..d76f9011 100644 --- a/README.md +++ b/README.md @@ -3683,6 +3683,21 @@ errorTextStyles: { } ``` +#### Overflow support for rendered images +By default, an image rendered by a File Element scales responsively to fit its `width`/`height`. If you'd rather show the image at its full/natural size inside a fixed-size, scrollable container, set `overflow` (e.g. `'auto'` or `'scroll'`) alongside `width`/`height` in `inputStyles.base`: + +```javascript +inputStyles: { + base: { + height: '250px', + width: '400px', + overflow: 'auto', // renders the image at full size and makes the container scrollable + }, +} +``` + +`overflow` is optional — if you don't set it, rendering behaves exactly as before. This only affects image renders (``); it has no effect on non-image files rendered via `` (e.g. PDFs). + ### Step 3: Mount Elements to the DOM Elements used for rendering files are mounted to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](https://github.com/skyflowapi/skyflow-js#step-3-mount-elements-to-the-dom). @@ -4303,6 +4318,10 @@ errorTextStyles: { } } ``` + +#### Overflow support for rendered images +Composable File Elements support the same `overflow` style property as regular File Elements. Refer to the [Overflow support for rendered images](https://github.com/skyflowapi/skyflow-js#overflow-support-for-rendered-images) section above. + ### Step 3: Mount Container to the DOM Mount Elements for file rendering to the DOM the same way as Elements used for revealing data. Refer to Step 3 of the [section above](#step-3-mount-container-to-the-dom). @@ -4351,7 +4370,9 @@ fetch("") altText: "This is an altText", }); // Step 3. - fileElement.mount("#renderFile"); // Assumes there is a placeholder div with id=renderFile on the page + // Note: for composable File Elements, `.mount()` is called on the container, + // not on the element returned by `.create()`. + container.mount("#renderFile"); // Assumes there is a placeholder div with id=renderFile on the page const renderButton = document.getElementById("renderFiles"); // button to call render file diff --git a/samples/using-script-tag/composable-file-render-overflow.html b/samples/using-script-tag/composable-file-render-overflow.html new file mode 100644 index 00000000..3e499dd0 --- /dev/null +++ b/samples/using-script-tag/composable-file-render-overflow.html @@ -0,0 +1,175 @@ + + + + + + + Skyflow Elements - Composable File Render Overflow + + + + +

Composable Render File Elements - Overflow

+ + + +

With overflow (image shown at full size, container scrolls)

+
+ +

Without overflow (default responsive sizing, unchanged for existing customers)

+
+ + +

+
+    
+  
+
diff --git a/samples/using-script-tag/file-render-overflow.html b/samples/using-script-tag/file-render-overflow.html
new file mode 100644
index 00000000..d224d676
--- /dev/null
+++ b/samples/using-script-tag/file-render-overflow.html
@@ -0,0 +1,163 @@
+
+
+  
+    
+    
+    
+    
+    File render elements - Overflow
+    
+  
+  
+    

Render File Elements - Overflow

+ +

With overflow (image shown at full size, container scrolls)

+
+ +

Without overflow (default responsive sizing, unchanged for existing customers)

+
+ + + + +