From 68542f26f4a38632230dd50cad6fa784e66d3f75 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Wed, 4 Jun 2025 08:46:25 -0600 Subject: [PATCH 1/3] feat: adds IterableEmbeddedMessageElements and associated classes --- .../IterableEmbeddedMessageDefaultAction.ts | 47 ++++++++++++ .../IterableEmbeddedMessageElements.ts | 71 +++++++++++++++++++ .../classes/IterableEmbeddedMessageText.ts | 45 ++++++++++++ src/embedded/classes/index.ts | 3 + 4 files changed, 166 insertions(+) create mode 100644 src/embedded/classes/IterableEmbeddedMessageDefaultAction.ts create mode 100644 src/embedded/classes/IterableEmbeddedMessageElements.ts create mode 100644 src/embedded/classes/IterableEmbeddedMessageText.ts diff --git a/src/embedded/classes/IterableEmbeddedMessageDefaultAction.ts b/src/embedded/classes/IterableEmbeddedMessageDefaultAction.ts new file mode 100644 index 000000000..486cbbb89 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessageDefaultAction.ts @@ -0,0 +1,47 @@ +export class IterableEmbeddedMessageDefaultAction { + /** + * The type of iterable action + * For custom actions, the type is `action://` prefix followed by a custom action name + */ + readonly type: string; + + /** + * The url for the action when the type is `openUrl` + * For custom actions, data is empty + */ + readonly data?: string; + + /** + * Creates an instance of `IterableEmbeddedMessageDefaultAction`. + * + * @param type - The type of iterable action + * @param data - The url for the action when the type is `openUrl` + */ + constructor(type: string, data?: string) { + this.type = type; + this.data = data; + } + + /** + * Creates an instance of `IterableEmbeddedMessageDefaultAction` from a dictionary object. + * + * @param dict - The dictionary object containing the properties to initialize the `IterableEmbeddedMessageDefaultAction` instance. + * @returns A new instance of `IterableEmbeddedMessageDefaultAction` initialized with the provided dictionary properties. + */ + static fromDict( + dict: Partial + ): IterableEmbeddedMessageDefaultAction { + if (!dict.type) { + throw new Error('type is required'); + } + return new IterableEmbeddedMessageDefaultAction(dict.type, dict.data); + } +} + +/** + * An interface defining the dictionary object containing the properties for the embedded message default action. + */ +export interface EmbeddedMessageDefaultActionDict { + type: string; + data?: string; +} diff --git a/src/embedded/classes/IterableEmbeddedMessageElements.ts b/src/embedded/classes/IterableEmbeddedMessageElements.ts new file mode 100644 index 000000000..226d6eb48 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessageElements.ts @@ -0,0 +1,71 @@ +import { IterableEmbeddedMessageDefaultAction } from './IterableEmbeddedMessageDefaultAction'; +import { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; +import { IterableEmbeddedMessageText } from './IterableEmbeddedMessageText'; + +export class IterableEmbeddedMessageElements { + readonly title?: string; + readonly body?: string; + readonly mediaUrl?: string; + readonly mediaUrlCaption?: string; + readonly defaultAction?: IterableEmbeddedMessageDefaultAction; + readonly buttons?: IterableEmbeddedMessageElementsButton[]; + readonly text?: IterableEmbeddedMessageText[]; + + constructor( + title?: string, + body?: string, + mediaUrl?: string, + mediaUrlCaption?: string, + defaultAction?: IterableEmbeddedMessageDefaultAction, + buttons?: IterableEmbeddedMessageElementsButton[], + text?: IterableEmbeddedMessageText[] + ) { + this.title = title; + this.body = body; + this.mediaUrl = mediaUrl; + this.mediaUrlCaption = mediaUrlCaption; + this.defaultAction = defaultAction; + this.buttons = buttons; + this.text = text; + } + + static fromDict( + dict: Partial + ): IterableEmbeddedMessageElements { + const title = dict.title; + const body = dict.body; + const mediaUrl = dict.mediaUrl; + const mediaUrlCaption = dict.mediaUrlCaption; + const defaultAction = dict.defaultAction + ? IterableEmbeddedMessageDefaultAction.fromDict(dict.defaultAction) + : undefined; + + const buttons = dict.buttons?.map((button) => + IterableEmbeddedMessageElementsButton.fromDict(button) + ); + + const text = dict.text?.map((text) => + IterableEmbeddedMessageText.fromDict(text) + ); + + return new IterableEmbeddedMessageElements( + title, + body, + mediaUrl, + mediaUrlCaption, + defaultAction, + buttons, + text + ); + } +} + +export interface EmbeddedMessageElementsDict { + title?: string; + body?: string; + mediaUrl?: string; + mediaUrlCaption?: string; + defaultAction?: IterableEmbeddedMessageDefaultAction; + buttons?: IterableEmbeddedMessageElementsButton[]; + text?: IterableEmbeddedMessageText[]; +} diff --git a/src/embedded/classes/IterableEmbeddedMessageText.ts b/src/embedded/classes/IterableEmbeddedMessageText.ts new file mode 100644 index 000000000..98c6a1b87 --- /dev/null +++ b/src/embedded/classes/IterableEmbeddedMessageText.ts @@ -0,0 +1,45 @@ +export class IterableEmbeddedMessageText { + /** The id of the text element */ + readonly id: string; + /** The text of the text element */ + readonly text?: string; + /** The type of the text element */ + readonly type?: string; + + /** + * Creates an instance of `IterableEmbeddedMessageText`. + * + * @param id - The id of the text element + * @param text - The text of the text element + * @param type - The type of the text element + */ + constructor(id: string, text?: string, type?: string) { + this.id = id; + this.text = text; + this.type = type; + } + + /** + * Creates an instance of `IterableEmbeddedMessageText` from a dictionary object. + * + * @param dict - The dictionary object containing the properties to initialize the `IterableEmbeddedMessageText` instance. + * @returns A new instance of `IterableEmbeddedMessageText` initialized with the provided dictionary properties. + */ + static fromDict( + dict: Partial + ): IterableEmbeddedMessageText { + if (!dict.id) { + throw new Error('id is required'); + } + return new IterableEmbeddedMessageText(dict.id, dict.text, dict.type); + } +} + +/** + * An interface defining the dictionary object containing the properties for an embedded message text. + */ +export interface EmbeddedMessageTextDict { + id: string; + text?: string; + type?: string; +} diff --git a/src/embedded/classes/index.ts b/src/embedded/classes/index.ts index 9b605f651..3b0ab3fa8 100644 --- a/src/embedded/classes/index.ts +++ b/src/embedded/classes/index.ts @@ -1,2 +1,5 @@ export * from './IterableEmbeddedManager'; export * from './IterableEmbeddedPlacement'; +export * from './IterableEmbeddedMessageElementsButton'; +export * from './IterableEmbeddedMessageElementsButtonAction'; +export * from './IterableEmbeddedMessageMetadata'; From 8d57b25e3d78cdfc872ad88cec63b15b707de23c Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Wed, 4 Jun 2025 08:58:17 -0600 Subject: [PATCH 2/3] feat: adds documentation --- .../IterableEmbeddedMessageDefaultAction.ts | 4 +++ .../IterableEmbeddedMessageElements.ts | 30 +++++++++++++++++++ ...ableEmbeddedMessageElementsButtonAction.ts | 4 +-- .../classes/IterableEmbeddedMessageText.ts | 3 ++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/embedded/classes/IterableEmbeddedMessageDefaultAction.ts b/src/embedded/classes/IterableEmbeddedMessageDefaultAction.ts index 486cbbb89..01275c56d 100644 --- a/src/embedded/classes/IterableEmbeddedMessageDefaultAction.ts +++ b/src/embedded/classes/IterableEmbeddedMessageDefaultAction.ts @@ -1,3 +1,7 @@ +/** + * IterableEmbeddedMessageDefaultAction represents the default action defined as + * a response to user events for an embedded message + */ export class IterableEmbeddedMessageDefaultAction { /** * The type of iterable action diff --git a/src/embedded/classes/IterableEmbeddedMessageElements.ts b/src/embedded/classes/IterableEmbeddedMessageElements.ts index 226d6eb48..eeb8887c9 100644 --- a/src/embedded/classes/IterableEmbeddedMessageElements.ts +++ b/src/embedded/classes/IterableEmbeddedMessageElements.ts @@ -2,15 +2,36 @@ import { IterableEmbeddedMessageDefaultAction } from './IterableEmbeddedMessageD import { IterableEmbeddedMessageElementsButton } from './IterableEmbeddedMessageElementsButton'; import { IterableEmbeddedMessageText } from './IterableEmbeddedMessageText'; +/** + * IterableEmbeddedMessageElements represents the elements of an embedded message. + */ export class IterableEmbeddedMessageElements { + /** The title of the embedded message */ readonly title?: string; + /** The body of the embedded message */ readonly body?: string; + /** The url of the embedded message image */ readonly mediaUrl?: string; + /** The caption of the embedded message image */ readonly mediaUrlCaption?: string; + /** The default action of the embedded message */ readonly defaultAction?: IterableEmbeddedMessageDefaultAction; + /** The buttons of the embedded message */ readonly buttons?: IterableEmbeddedMessageElementsButton[]; + /** The text elements of the embedded message */ readonly text?: IterableEmbeddedMessageText[]; + /** + * Creates an instance of `IterableEmbeddedMessageElements`. + * + * @param title - The title of the embedded message. + * @param body - The body of the embedded message. + * @param mediaUrl - The url of the embedded message image. + * @param mediaUrlCaption - The caption of the embedded message image. + * @param defaultAction - The default action of the embedded message. + * @param buttons - The buttons of the embedded message. + * @param text - The text elements of the embedded message. + */ constructor( title?: string, body?: string, @@ -29,6 +50,12 @@ export class IterableEmbeddedMessageElements { this.text = text; } + /** + * Creates an instance of `IterableEmbeddedMessageElements` from a dictionary object. + * + * @param dict - The dictionary object containing the properties to initialize the `IterableEmbeddedMessageElements` instance. + * @returns A new instance of `IterableEmbeddedMessageElements` initialized with the provided dictionary properties. + */ static fromDict( dict: Partial ): IterableEmbeddedMessageElements { @@ -60,6 +87,9 @@ export class IterableEmbeddedMessageElements { } } +/** + * An interface defining the dictionary object containing the properties for the embedded message elements. + */ export interface EmbeddedMessageElementsDict { title?: string; body?: string; diff --git a/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts b/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts index c8dd24708..a924ffec5 100644 --- a/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts +++ b/src/embedded/classes/IterableEmbeddedMessageElementsButtonAction.ts @@ -1,6 +1,6 @@ /** - * IterableEmbeddedMessageElementsButtonAction represents an action defined as a response to user events - * for an embedded message button. + * IterableEmbeddedMessageElementsButtonAction represents an action defined as + * a response to user events for an embedded message button */ export class IterableEmbeddedMessageElementsButtonAction { /** diff --git a/src/embedded/classes/IterableEmbeddedMessageText.ts b/src/embedded/classes/IterableEmbeddedMessageText.ts index 98c6a1b87..d8a878519 100644 --- a/src/embedded/classes/IterableEmbeddedMessageText.ts +++ b/src/embedded/classes/IterableEmbeddedMessageText.ts @@ -1,3 +1,6 @@ +/** + * IterableEmbeddedMessageText represents a text element in an embedded message. + */ export class IterableEmbeddedMessageText { /** The id of the text element */ readonly id: string; From c857da5947c175eb29e94ceea8cf6437ea61f34e Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Wed, 4 Jun 2025 09:24:38 -0600 Subject: [PATCH 3/3] feat: adds unit tests --- ...erableEmbeddedMessageDefaultAction.test.ts | 40 ++++ .../IterableEmbeddedMessageElements.test.ts | 214 ++++++++++++++++++ .../IterableEmbeddedMessageText.test.ts | 38 ++++ 3 files changed, 292 insertions(+) create mode 100644 src/__tests__/IterableEmbeddedMessageDefaultAction.test.ts create mode 100644 src/__tests__/IterableEmbeddedMessageElements.test.ts create mode 100644 src/__tests__/IterableEmbeddedMessageText.test.ts diff --git a/src/__tests__/IterableEmbeddedMessageDefaultAction.test.ts b/src/__tests__/IterableEmbeddedMessageDefaultAction.test.ts new file mode 100644 index 000000000..bf99bc5b8 --- /dev/null +++ b/src/__tests__/IterableEmbeddedMessageDefaultAction.test.ts @@ -0,0 +1,40 @@ +import { IterableEmbeddedMessageDefaultAction } from '../embedded/classes/IterableEmbeddedMessageDefaultAction'; +import { Iterable } from '../core/classes/Iterable'; + +describe('IterableEmbeddedMessageDefaultAction', () => { + it('should create an instance with the correct properties', () => { + Iterable.logger.log( + 'iterableEmbeddedMessageDefaultAction_fromDict_valid_dictionary' + ); + + const dict = { type: 'openUrl', data: 'https://example.com' }; + const action = IterableEmbeddedMessageDefaultAction.fromDict(dict); + expect(action).toBeInstanceOf(IterableEmbeddedMessageDefaultAction); + expect(action.type).toBe('openUrl'); + expect(action.data).toBe('https://example.com'); + }); + + it('should create an instance from a dictionary with data omitted', () => { + Iterable.logger.log( + 'iterableEmbeddedMessageDefaultAction_fromDict_valid_dictionary_with_data_omitted' + ); + + const dict = { type: 'action://join', data: '' }; + const action = IterableEmbeddedMessageDefaultAction.fromDict(dict); + expect(action).toBeInstanceOf(IterableEmbeddedMessageDefaultAction); + expect(action.type).toBe('action://join'); + expect(action.data).toBe(''); + }); + + it('should throw an error if type is missing in fromDict', () => { + Iterable.logger.log( + 'iterableEmbeddedMessageDefaultAction_fromDict_invalid_dictionary_missing_type' + ); + + const dict = { data: 'foo' }; + + expect(() => IterableEmbeddedMessageDefaultAction.fromDict(dict)).toThrow( + 'type is required' + ); + }); +}); diff --git a/src/__tests__/IterableEmbeddedMessageElements.test.ts b/src/__tests__/IterableEmbeddedMessageElements.test.ts new file mode 100644 index 000000000..00028da5f --- /dev/null +++ b/src/__tests__/IterableEmbeddedMessageElements.test.ts @@ -0,0 +1,214 @@ +import { IterableEmbeddedMessageElements } from '../embedded/classes/IterableEmbeddedMessageElements'; +import { IterableEmbeddedMessageDefaultAction } from '../embedded/classes/IterableEmbeddedMessageDefaultAction'; +import { IterableEmbeddedMessageElementsButton } from '../embedded/classes/IterableEmbeddedMessageElementsButton'; +import { IterableEmbeddedMessageText } from '../embedded/classes/IterableEmbeddedMessageText'; +import { Iterable } from '../core/classes/Iterable'; + +describe('IterableEmbeddedMessageElements', () => { + it('should create an instance with all properties', () => { + Iterable.logger.log( + 'iterableEmbeddedMessageElements_fromDict_all_properties' + ); + + const dict = { + title: 'Awesome Title', + body: 'Radical Body Text', + mediaUrl: 'https://example.com/image.jpg', + mediaUrlCaption: 'Check out this sick image!', + defaultAction: { + type: 'openUrl', + data: 'https://example.com', + }, + buttons: [ + { + id: 'button-1', + title: 'Click Me!', + action: { + type: 'openUrl', + data: 'https://example.com/button1', + }, + }, + { + id: 'button-2', + title: 'Close', + action: { + type: 'action://dismiss', + }, + }, + ], + text: [ + { + id: 'text-1', + text: 'Some cool text', + type: 'body', + }, + { + id: 'text-2', + text: 'More radical text', + type: 'subtitle', + }, + ], + }; + + const elements = IterableEmbeddedMessageElements.fromDict(dict); + + expect(elements).toBeInstanceOf(IterableEmbeddedMessageElements); + expect(elements.title).toBe('Awesome Title'); + expect(elements.body).toBe('Radical Body Text'); + expect(elements.mediaUrl).toBe('https://example.com/image.jpg'); + expect(elements.mediaUrlCaption).toBe('Check out this sick image!'); + + // Check defaultAction + expect(elements.defaultAction).toBeInstanceOf( + IterableEmbeddedMessageDefaultAction + ); + expect(elements.defaultAction?.type).toBe('openUrl'); + expect(elements.defaultAction?.data).toBe('https://example.com'); + + // Check buttons + expect(elements.buttons).toHaveLength(2); + const firstButton = elements + .buttons![0] as IterableEmbeddedMessageElementsButton; + expect(firstButton).toBeInstanceOf(IterableEmbeddedMessageElementsButton); + expect(firstButton.id).toBe('button-1'); + expect(firstButton.title).toBe('Click Me!'); + expect(firstButton.action?.type).toBe('openUrl'); + expect(firstButton.action?.data).toBe('https://example.com/button1'); + + const secondButton = elements + .buttons![1] as IterableEmbeddedMessageElementsButton; + expect(secondButton).toBeInstanceOf(IterableEmbeddedMessageElementsButton); + expect(secondButton.id).toBe('button-2'); + expect(secondButton.title).toBe('Close'); + expect(secondButton.action?.type).toBe('action://dismiss'); + expect(secondButton.action?.data).toBeUndefined(); + + // Check text elements + expect(elements.text).toHaveLength(2); + const firstText = elements.text![0] as IterableEmbeddedMessageText; + expect(firstText).toBeInstanceOf(IterableEmbeddedMessageText); + expect(firstText.id).toBe('text-1'); + expect(firstText.text).toBe('Some cool text'); + expect(firstText.type).toBe('body'); + + const secondText = elements.text![1] as IterableEmbeddedMessageText; + expect(secondText).toBeInstanceOf(IterableEmbeddedMessageText); + expect(secondText.id).toBe('text-2'); + expect(secondText.text).toBe('More radical text'); + expect(secondText.type).toBe('subtitle'); + }); + + it('should create an instance with title and body', () => { + Iterable.logger.log( + 'iterableEmbeddedMessageElements_fromDict_title_and_body' + ); + + const dict = { + title: 'Simple Title', + body: 'Simple Body', + }; + + const elements = IterableEmbeddedMessageElements.fromDict(dict); + + expect(elements).toBeInstanceOf(IterableEmbeddedMessageElements); + expect(elements.title).toBe('Simple Title'); + expect(elements.body).toBe('Simple Body'); + expect(elements.mediaUrl).toBeUndefined(); + expect(elements.mediaUrlCaption).toBeUndefined(); + expect(elements.defaultAction).toBeUndefined(); + expect(elements.buttons).toBeUndefined(); + expect(elements.text).toBeUndefined(); + }); + + it('should create an instance with no title or body', () => { + Iterable.logger.log( + 'iterableEmbeddedMessageElements_fromDict_no_title_or_body' + ); + + const dict = {}; + + const elements = IterableEmbeddedMessageElements.fromDict(dict); + + expect(elements).toBeInstanceOf(IterableEmbeddedMessageElements); + expect(elements.title).toBeUndefined(); + expect(elements.body).toBeUndefined(); + expect(elements.mediaUrl).toBeUndefined(); + expect(elements.mediaUrlCaption).toBeUndefined(); + expect(elements.defaultAction).toBeUndefined(); + expect(elements.buttons).toBeUndefined(); + expect(elements.text).toBeUndefined(); + }); + + it('should create an instance with media properties', () => { + Iterable.logger.log( + 'iterableEmbeddedMessageElements_fromDict_media_properties' + ); + + const dict = { + title: 'Media Title', + body: 'Media Body', + mediaUrl: 'https://example.com/media.jpg', + mediaUrlCaption: 'Check this out!', + }; + + const elements = IterableEmbeddedMessageElements.fromDict(dict); + + expect(elements).toBeInstanceOf(IterableEmbeddedMessageElements); + expect(elements.title).toBe('Media Title'); + expect(elements.body).toBe('Media Body'); + expect(elements.mediaUrl).toBe('https://example.com/media.jpg'); + expect(elements.mediaUrlCaption).toBe('Check this out!'); + expect(elements.defaultAction).toBeUndefined(); + expect(elements.buttons).toBeUndefined(); + expect(elements.text).toBeUndefined(); + }); + + it('should create an instance with defaultAction only', () => { + Iterable.logger.log( + 'iterableEmbeddedMessageElements_fromDict_defaultAction_only' + ); + + const dict = { + title: 'Action Title', + body: 'Action Body', + defaultAction: { + type: 'openUrl', + data: 'https://example.com', + }, + }; + + const elements = IterableEmbeddedMessageElements.fromDict(dict); + + expect(elements).toBeInstanceOf(IterableEmbeddedMessageElements); + expect(elements.title).toBe('Action Title'); + expect(elements.body).toBe('Action Body'); + expect(elements.defaultAction).toBeInstanceOf( + IterableEmbeddedMessageDefaultAction + ); + expect(elements.defaultAction?.type).toBe('openUrl'); + expect(elements.defaultAction?.data).toBe('https://example.com'); + expect(elements.buttons).toBeUndefined(); + expect(elements.text).toBeUndefined(); + }); + + it('should create an instance with empty arrays for buttons and text', () => { + Iterable.logger.log( + 'iterableEmbeddedMessageElements_fromDict_empty_arrays' + ); + + const dict = { + title: 'Empty Arrays Title', + body: 'Empty Arrays Body', + buttons: [], + text: [], + }; + + const elements = IterableEmbeddedMessageElements.fromDict(dict); + + expect(elements).toBeInstanceOf(IterableEmbeddedMessageElements); + expect(elements.title).toBe('Empty Arrays Title'); + expect(elements.body).toBe('Empty Arrays Body'); + expect(elements.buttons).toHaveLength(0); + expect(elements.text).toHaveLength(0); + }); +}); diff --git a/src/__tests__/IterableEmbeddedMessageText.test.ts b/src/__tests__/IterableEmbeddedMessageText.test.ts new file mode 100644 index 000000000..10b3e2ffe --- /dev/null +++ b/src/__tests__/IterableEmbeddedMessageText.test.ts @@ -0,0 +1,38 @@ +import { IterableEmbeddedMessageText } from '../embedded/classes/IterableEmbeddedMessageText'; +import { Iterable } from '../core/classes/Iterable'; + +describe('IterableEmbeddedMessageText', () => { + it('should create an instance from a dictionary with all properties', () => { + Iterable.logger.log('iterableEmbeddedMessageText_fromDict_all_properties'); + + const dict = { id: 'text-123', text: 'Hello World!', type: 'heading' }; + const text = IterableEmbeddedMessageText.fromDict(dict); + + expect(text).toBeInstanceOf(IterableEmbeddedMessageText); + expect(text.id).toBe('text-123'); + expect(text.text).toBe('Hello World!'); + expect(text.type).toBe('heading'); + }); + + it('should create an instance from a dictionary with only required properties', () => { + Iterable.logger.log('iterableEmbeddedMessageText_fromDict_required_only'); + + const dict = { id: 'text-123' }; + const text = IterableEmbeddedMessageText.fromDict(dict); + + expect(text).toBeInstanceOf(IterableEmbeddedMessageText); + expect(text.id).toBe('text-123'); + expect(text.text).toBeUndefined(); + expect(text.type).toBeUndefined(); + }); + + it('should throw an error if id is missing in fromDict', () => { + Iterable.logger.log('iterableEmbeddedMessageText_fromDict_missing_id'); + + const dict = { text: 'Hello World!', type: 'heading' }; + + expect(() => IterableEmbeddedMessageText.fromDict(dict)).toThrow( + 'id is required' + ); + }); +});