Skip to content

Commit 4d87add

Browse files
committed
feat: Enhance Bluesky component with unique iframe IDs and height adjustment logic
- Implemented unique ID generation for each iframe instance to ensure proper height updates. - Updated message handling to only adjust height for the specific iframe that sent the message. - Added comprehensive tests to verify functionality for multiple instances and edge cases, ensuring robustness in height adjustments and iframe identification.
1 parent 5e36493 commit 4d87add

File tree

2 files changed

+271
-11
lines changed

2 files changed

+271
-11
lines changed

packages/sveltekit-embed/src/lib/components/bluesky.svelte

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,32 @@
1414
}: Props = $props();
1515
1616
let wrapper_height = $state('174.5px');
17+
let iframe_id = $state('');
1718
1819
const get_embed_url = (post_id: string) => {
1920
return `https://embed.bsky.app/embed/${post_id}`;
2021
};
2122
2223
onMount(() => {
24+
// Generate a unique ID for this iframe instance
25+
iframe_id = `bluesky-iframe-${Math.random().toString(36).substr(2, 9)}`;
26+
2327
const handle_message = (event: MessageEvent) => {
2428
if (event.origin !== 'https://embed.bsky.app') return;
2529
2630
if (typeof event.data === 'object') {
27-
wrapper_height = `${event.data.height || event.data.h || 500}px`;
31+
// Check if this message is for this specific iframe
32+
// Bluesky embeds don't send iframe IDs, so we need to use event.source
33+
// to identify which iframe sent the message
34+
const iframe_element = document.getElementById(
35+
iframe_id,
36+
) as HTMLIFrameElement;
37+
if (
38+
iframe_element &&
39+
event.source === iframe_element.contentWindow
40+
) {
41+
wrapper_height = `${event.data.height || event.data.h || 500}px`;
42+
}
2843
}
2944
};
3045
@@ -38,6 +53,7 @@
3853
<div class="bluesky-wrapper-container">
3954
<div class="bluesky-wrapper" style={`height: ${wrapper_height}`}>
4055
<iframe
56+
id={iframe_id}
4157
data-testid="bluesky-embed"
4258
title="Bluesky Post Embed"
4359
src={get_embed_url(post_id)}

packages/sveltekit-embed/src/lib/components/bluesky.svelte.test.ts

Lines changed: 254 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { page } from '@vitest/browser/context';
12
import { describe, expect, it, vi } from 'vitest';
23
import { render } from 'vitest-browser-svelte';
3-
import { page } from '@vitest/browser/context';
44
import Bluesky from './bluesky.svelte';
55

66
describe('Bluesky', () => {
@@ -40,7 +40,7 @@ describe('Bluesky', () => {
4040
});
4141

4242
const iframe = page.getByTestId('bluesky-embed');
43-
const element = iframe.element();
43+
const element = iframe.element() as HTMLElement;
4444
const style_text = element.style.cssText.toLowerCase();
4545
expect(style_text).toContain('border-radius: 8px');
4646
expect(style_text).toContain('background: rgb(240, 240, 240)');
@@ -52,7 +52,7 @@ describe('Bluesky', () => {
5252
});
5353

5454
const iframe = page.getByTestId('bluesky-embed');
55-
const element = iframe.element();
55+
const element = iframe.element() as HTMLElement;
5656
const style_text = element.style.cssText.toLowerCase();
5757
expect(style_text).toContain('position: absolute');
5858
expect(style_text).toContain('top: 0px');
@@ -72,7 +72,7 @@ describe('Bluesky', () => {
7272
});
7373

7474
const iframe = page.getByTestId('bluesky-embed');
75-
const element = iframe.element();
75+
const element = iframe.element() as HTMLElement;
7676
const style_text = element.style.cssText.toLowerCase();
7777

7878
// Check default styles are preserved
@@ -87,20 +87,47 @@ describe('Bluesky', () => {
8787
});
8888

8989
it('updates height when receiving message from iframe', async () => {
90-
render(Bluesky, {
90+
const { container } = render(Bluesky, {
9191
post_id: test_post_id,
9292
});
9393

94+
const wrapper = container.querySelector(
95+
'.bluesky-wrapper',
96+
) as HTMLElement;
97+
const iframe = container.querySelector(
98+
'iframe',
99+
) as HTMLIFrameElement;
100+
101+
// Wait for component to mount and event listener to be added
102+
await new Promise(resolve => setTimeout(resolve, 0));
103+
104+
// Create a simple message event without the problematic source property
105+
// The component logic will still work since it checks event.origin first
94106
const message_event = new MessageEvent('message', {
95107
data: { type: 'height', height: 500 },
96108
origin: 'https://embed.bsky.app',
97109
});
98110

111+
// Mock the iframe's contentWindow to match what the component expects
112+
const mockContentWindow = {} as Window;
113+
Object.defineProperty(iframe, 'contentWindow', {
114+
value: mockContentWindow,
115+
writable: true,
116+
});
117+
118+
// Override the event's source property after creation
119+
Object.defineProperty(message_event, 'source', {
120+
value: mockContentWindow,
121+
writable: false,
122+
});
123+
99124
window.dispatchEvent(message_event);
100125

101-
const iframe = page.getByTestId('bluesky-embed');
102-
const element = iframe.element();
103-
expect(element.style.height).toBe('100%');
126+
// Wait for DOM update
127+
await new Promise(resolve => setTimeout(resolve, 0));
128+
129+
// Check that the wrapper height was updated
130+
expect(wrapper?.style.height).toBe('500px');
104131
});
105132

106133
// Edge Cases and Comprehensive Coverage
@@ -145,16 +172,32 @@ describe('Bluesky', () => {
145172
const wrapper = container.querySelector(
146173
'.bluesky-wrapper',
147174
) as HTMLElement;
175+
const iframe = container.querySelector(
176+
'iframe',
177+
) as HTMLIFrameElement;
148178

149179
// Wait for component to mount and event listener to be added
150180
await new Promise(resolve => setTimeout(resolve, 0));
151181

152-
// Send message with 'h' property instead of 'height'
182+
// Create a simple message event
153183
const message_event = new MessageEvent('message', {
154184
data: { h: 350 },
155185
origin: 'https://embed.bsky.app',
156186
});
157187

188+
// Mock the iframe's contentWindow
189+
const mockContentWindow = {} as Window;
190+
Object.defineProperty(iframe, 'contentWindow', {
191+
value: mockContentWindow,
192+
writable: true,
193+
});
194+
195+
// Override the event's source property after creation
196+
Object.defineProperty(message_event, 'source', {
197+
value: mockContentWindow,
198+
writable: false,
199+
});
200+
158201
window.dispatchEvent(message_event);
159202

160203
// Wait for DOM update
@@ -172,16 +215,32 @@ describe('Bluesky', () => {
172215
const wrapper = container.querySelector(
173216
'.bluesky-wrapper',
174217
) as HTMLElement;
218+
const iframe = container.querySelector(
219+
'iframe',
220+
) as HTMLIFrameElement;
175221

176222
// Wait for component to mount and event listener to be added
177223
await new Promise(resolve => setTimeout(resolve, 0));
178224

179-
// Send message with no height or h property
225+
// Create a simple message event
180226
const invalid_message = new MessageEvent('message', {
181227
data: { type: 'invalid' },
182228
origin: 'https://embed.bsky.app',
183229
});
184230

231+
// Mock the iframe's contentWindow
232+
const mockContentWindow = {} as Window;
233+
Object.defineProperty(iframe, 'contentWindow', {
234+
value: mockContentWindow,
235+
writable: true,
236+
});
237+
238+
// Override the event's source property after creation
239+
Object.defineProperty(invalid_message, 'source', {
240+
value: mockContentWindow,
241+
writable: false,
242+
});
243+
185244
window.dispatchEvent(invalid_message);
186245

187246
// Wait for DOM update
@@ -214,6 +273,191 @@ describe('Bluesky', () => {
214273
removeEventListenerSpy.mockRestore();
215274
});
216275

276+
it('should generate unique iframe IDs for multiple instances', async () => {
277+
const { container: container1 } = render(Bluesky, {
278+
post_id: test_post_id,
279+
});
280+
281+
const { container: container2 } = render(Bluesky, {
282+
post_id: 'did:plc:different/app.bsky.feed.post/different',
283+
});
284+
285+
const iframe1 = container1.querySelector(
286+
'iframe',
287+
) as HTMLIFrameElement;
288+
const iframe2 = container2.querySelector(
289+
'iframe',
290+
) as HTMLIFrameElement;
291+
292+
expect(iframe1.id).toBeTruthy();
293+
expect(iframe2.id).toBeTruthy();
294+
expect(iframe1.id).not.toBe(iframe2.id);
295+
expect(iframe1.id).toMatch(/^bluesky-iframe-[a-z0-9]{9}$/);
296+
expect(iframe2.id).toMatch(/^bluesky-iframe-[a-z0-9]{9}$/);
297+
});
298+
299+
it('should only update height for the specific iframe that sent the message', async () => {
300+
const { container: container1 } = render(Bluesky, {
301+
post_id: test_post_id,
302+
});
303+
304+
const { container: container2 } = render(Bluesky, {
305+
post_id: 'did:plc:different/app.bsky.feed.post/different',
306+
});
307+
308+
const wrapper1 = container1.querySelector(
309+
'.bluesky-wrapper',
310+
) as HTMLElement;
311+
const wrapper2 = container2.querySelector(
312+
'.bluesky-wrapper',
313+
) as HTMLElement;
314+
const iframe1 = container1.querySelector(
315+
'iframe',
316+
) as HTMLIFrameElement;
317+
const iframe2 = container2.querySelector(
318+
'iframe',
319+
) as HTMLIFrameElement;
320+
321+
// Wait for components to mount and event listeners to be added
322+
await new Promise(resolve => setTimeout(resolve, 0));
323+
324+
// Create mock window objects for both iframes
325+
const mockContentWindow1 = {} as Window;
326+
const mockContentWindow2 = {} as Window;
327+
328+
Object.defineProperty(iframe1, 'contentWindow', {
329+
value: mockContentWindow1,
330+
writable: true,
331+
});
332+
333+
Object.defineProperty(iframe2, 'contentWindow', {
334+
value: mockContentWindow2,
335+
writable: true,
336+
});
337+
338+
// Send message that appears to come from iframe1
339+
const message_event_1 = new MessageEvent('message', {
340+
data: { height: 300 },
341+
origin: 'https://embed.bsky.app',
342+
});
343+
344+
// Override the event's source property
345+
Object.defineProperty(message_event_1, 'source', {
346+
value: mockContentWindow1,
347+
writable: false,
348+
});
349+
350+
window.dispatchEvent(message_event_1);
351+
352+
// Wait for DOM update
353+
await new Promise(resolve => setTimeout(resolve, 0));
354+
355+
// Only wrapper1 should update, wrapper2 should remain unchanged
356+
expect(wrapper1.style.height).toBe('300px');
357+
expect(wrapper2.style.height).toBe('174.5px');
358+
359+
// Now send message that appears to come from iframe2
360+
const message_event_2 = new MessageEvent('message', {
361+
data: { height: 450 },
362+
origin: 'https://embed.bsky.app',
363+
});
364+
365+
// Override the event's source property
366+
Object.defineProperty(message_event_2, 'source', {
367+
value: mockContentWindow2,
368+
writable: false,
369+
});
370+
371+
window.dispatchEvent(message_event_2);
372+
373+
// Wait for DOM update
374+
await new Promise(resolve => setTimeout(resolve, 0));
375+
376+
// wrapper1 should remain at 300px, wrapper2 should update to 450px
377+
expect(wrapper1.style.height).toBe('300px');
378+
expect(wrapper2.style.height).toBe('450px');
379+
});
380+
381+
it('should ignore messages from unknown iframe sources', async () => {
382+
const { container } = render(Bluesky, {
383+
post_id: test_post_id,
384+
});
385+
386+
const wrapper = container.querySelector(
387+
'.bluesky-wrapper',
388+
) as HTMLElement;
389+
const iframe = container.querySelector(
390+
'iframe',
391+
) as HTMLIFrameElement;
392+
const initial_height = wrapper.style.height;
393+
394+
// Wait for component to mount and event listener to be added
395+
await new Promise(resolve => setTimeout(resolve, 0));
396+
397+
// Mock the iframe's contentWindow
398+
const mockContentWindow = {} as Window;
399+
Object.defineProperty(iframe, 'contentWindow', {
400+
value: mockContentWindow,
401+
writable: true,
402+
});
403+
404+
// Create an unknown window source (different from any iframe's contentWindow)
405+
const unknown_source = {} as Window;
406+
407+
const message_event = new MessageEvent('message', {
408+
data: { height: 600 },
409+
origin: 'https://embed.bsky.app',
410+
});
411+
412+
// Override the event's source property with unknown source
413+
Object.defineProperty(message_event, 'source', {
414+
value: unknown_source,
415+
writable: false,
416+
});
417+
418+
window.dispatchEvent(message_event);
419+
420+
// Wait for DOM update
421+
await new Promise(resolve => setTimeout(resolve, 0));
422+
423+
// Height should remain unchanged
424+
expect(wrapper.style.height).toBe(initial_height);
425+
});
426+
427+
it('should handle multiple instances with different post IDs correctly', async () => {
428+
const post_ids = [
429+
'did:plc:x7cbjcbvndpjb3vyndsqgpsi/app.bsky.feed.post/3lpzm7ynvzs2o',
430+
'did:plc:x7cbjcbvndpjb3vyndsqgpsi/app.bsky.feed.post/3lq2ypghyrk2p',
431+
'did:plc:x7cbjcbvndpjb3vyndsqgpsi/app.bsky.feed.post/3lq2yv3uzjc2p',
432+
];
433+
434+
const instances = post_ids.map(post_id =>
435+
render(Bluesky, { post_id }),
436+
);
437+
438+
// Wait for all components to mount
439+
await new Promise(resolve => setTimeout(resolve, 0));
440+
441+
// Verify each instance has unique iframe ID and correct src
442+
instances.forEach(({ container }, index) => {
443+
const iframe = container.querySelector(
444+
'iframe',
445+
) as HTMLIFrameElement;
446+
expect(iframe.id).toMatch(/^bluesky-iframe-[a-z0-9]{9}$/);
447+
expect(iframe.src).toBe(
448+
`https://embed.bsky.app/embed/${post_ids[index]}`,
449+
);
450+
});
451+
452+
// Verify all iframe IDs are unique
453+
const iframe_ids = instances.map(
454+
({ container }) =>
455+
(container.querySelector('iframe') as HTMLIFrameElement).id,
456+
);
457+
const unique_ids = new Set(iframe_ids);
458+
expect(unique_ids.size).toBe(post_ids.length);
459+
});
460+
217461
it('should handle very long post_id values', async () => {
218462
const long_post_id =
219463
'did:plc:' +

0 commit comments

Comments
 (0)