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)
+
+
+
+ Render Files
+
+
+
+
+
+
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)
+
+
+
+ Render Files
+
+
+
+
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..814d872c 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,30 @@ 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: "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;
+ });
});
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