From 3d54d67bbccb22d84e82b92020db488a0d83c9a4 Mon Sep 17 00:00:00 2001 From: alexneyman Date: Thu, 19 Dec 2024 12:25:51 -0500 Subject: [PATCH 1/2] upd --- .../e2e-test-data/exampleFeature.json | 56 +++++++++++ .../src/components/ExampleFeatureAPIs.tsx | 95 +++++++++++++++++++ apps/teams-test-app/src/pages/TestApp.tsx | 2 + .../teams-js/src/private/exampleFeature.ts | 90 ++++++++++++++++++ packages/teams-js/src/private/index.ts | 1 + packages/teams-js/src/public/runtime.ts | 22 ++++- 6 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 apps/teams-test-app/e2e-test-data/exampleFeature.json create mode 100644 apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx create mode 100644 packages/teams-js/src/private/exampleFeature.ts diff --git a/apps/teams-test-app/e2e-test-data/exampleFeature.json b/apps/teams-test-app/e2e-test-data/exampleFeature.json new file mode 100644 index 0000000000..a31b1de503 --- /dev/null +++ b/apps/teams-test-app/e2e-test-data/exampleFeature.json @@ -0,0 +1,56 @@ +{ + "name": "Example Feature", + "checkIsSupported": { + "version": ">2.0.0-beta.3" + }, + "featureTests": [ + { + "feature": { + "id": "exampleFeature", + "version": 1 + }, + "testCases": [ + { + "title": "Basic Call - Success", + "type": "callResponse", + "boxSelector": "#box_exampleFeature", + "inputValue": { + "input": "test input" + }, + "expectedTestAppValue": "{\"status\":\"test successful - received: test input\"}" + }, + { + "title": "Basic Call - Missing Input", + "type": "callResponse", + "boxSelector": "#box_exampleFeature", + "inputValue": {}, + "expectedTestAppValue": "Error: Input is required" + }, + { + "title": "Register and Raise Event", + "type": "callResponse", + "boxSelector": "#box_exampleFeatureEvent", + "inputValue": { + "data": "test data" + }, + "expectedTestAppValue": "event received: test data" + }, + { + "title": "Raise Direct Event", + "type": "callResponse", + "boxSelector": "#box_exampleDirectEvent", + "expectedTestAppValue": "Event raised" + }, + { + "title": "Regular Test", + "type": "callResponse", + "boxSelector": "#box_regularTest", + "inputValue": { + "test": "test input" + }, + "expectedTestAppValue": "regular test complete" + } + ] + } + ] +} diff --git a/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx b/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx new file mode 100644 index 0000000000..59833861fe --- /dev/null +++ b/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx @@ -0,0 +1,95 @@ +import { exampleFeature } from '@microsoft/teams-js'; +import React from 'react'; + +import { ApiWithoutInput, ApiWithTextInput } from './utils'; +import { ModuleWrapper } from './utils/ModuleWrapper'; + +const CheckExampleFeatureCapability = (): React.ReactElement => + ApiWithoutInput({ + name: 'checkExampleFeatureCapability', + title: 'Check Example Feature Capability', + onClick: async () => `Example Feature module ${exampleFeature.isSupported() ? 'is' : 'is not'} supported`, + }); + +const BasicCall = (): React.ReactElement => + ApiWithTextInput<{ input: string }>({ + name: 'exampleFeature', + title: 'Basic Call', + onClick: { + validateInput: (input) => { + if (!input.input) { + throw new Error('Input is required'); + } + }, + submit: async (input) => { + const response = await exampleFeature.basicCall(input); + return response.status; + }, + }, + defaultInput: JSON.stringify({ input: 'test input' }), + }); + +const RegisterAndRaiseEvent = (): React.ReactElement => + ApiWithTextInput<{ data: string }>({ + name: 'exampleFeatureEvent', + title: 'Register and Raise Event', + onClick: { + validateInput: (input) => { + if (!input.data) { + throw new Error('Data is required'); + } + }, + submit: async (input) => { + return new Promise((resolve) => { + exampleFeature.registerEventHandler((data) => { + resolve(`event received: ${data.data}`); + }); + window.dispatchEvent( + new CustomEvent('exampleEvent', { + detail: { data: input.data }, + }), + ); + }); + }, + }, + defaultInput: JSON.stringify({ data: 'test data' }), + }); + +const RaiseDirectEvent = (): React.ReactElement => + ApiWithoutInput({ + name: 'exampleDirectEvent', + title: 'Raise Direct Event', + onClick: async () => { + exampleFeature.raiseEvent('direct event data'); + return 'Event raised'; + }, + }); + +const RegularTest = (): React.ReactElement => + ApiWithTextInput({ + name: 'regularTest', + title: 'Regular Test', + onClick: { + validateInput: (input) => { + if (!input) { + throw new Error('Input is required'); + } + }, + submit: async () => { + return 'regular test complete'; + }, + }, + defaultInput: JSON.stringify({ test: 'test input' }), + }); + +const ExampleFeatureAPIs = (): React.ReactElement => ( + + + + + + + +); + +export default ExampleFeatureAPIs; diff --git a/apps/teams-test-app/src/pages/TestApp.tsx b/apps/teams-test-app/src/pages/TestApp.tsx index f9af8507fb..59d5c59f37 100644 --- a/apps/teams-test-app/src/pages/TestApp.tsx +++ b/apps/teams-test-app/src/pages/TestApp.tsx @@ -18,6 +18,7 @@ import DialogUpdateAPIs from '../components/DialogUpdateAPIs'; import DialogUrlAPIs from '../components/DialogUrlAPIs'; import DialogUrlBotAPIs from '../components/DialogUrlBotAPIs'; import DialogUrlParentCommunicationAPIs from '../components/DialogUrlParentCommunicationAPIs'; +import ExampleFeatureAPIs from '../components/ExampleFeatureAPIs'; import GeoLocationAPIs from '../components/GeoLocationAPIs'; import HostEntityTabAPIs from '../components/HostEntityTabAPIs'; import Links from '../components/Links'; @@ -96,6 +97,7 @@ export const TestApp: React.FC = () => { // List of sections dynamically created from React elements const sections = useMemo( () => [ + { name: 'ExampleFeatureAPIs', component: }, { name: 'AppAPIs', component: }, { name: 'AppInitializationAPIs', component: }, { name: 'AppInstallDialogAPIs', component: }, diff --git a/packages/teams-js/src/private/exampleFeature.ts b/packages/teams-js/src/private/exampleFeature.ts new file mode 100644 index 0000000000..f637ac42e1 --- /dev/null +++ b/packages/teams-js/src/private/exampleFeature.ts @@ -0,0 +1,90 @@ +import { ensureInitialized } from '../internal/internalAPIs'; +import { errorNotSupportedOnPlatform } from '../public/constants'; +import { ErrorCode } from '../public/interfaces'; +import { runtime } from '../public/runtime'; + +/** + * @internal + * Limited to Microsoft-internal use + */ +export interface ExampleResponse { + /** + * Status message returned from the call + */ + status: string; +} + +/** + * @internal + * Limited to Microsoft-internal use + */ +export interface ExampleInput { + /** + * Input string to send with the call + */ + input: string; +} + +/** + * @internal + * Limited to Microsoft-internal use + */ +export interface ExampleEventData { + /** + * Data payload for the event + */ + data: string; +} + +/** + * @internal + * Limited to Microsoft-internal use + */ +export function isSupported(): boolean { + return ensureInitialized(runtime) && runtime.supports.exampleFeature?.basicCall ? true : false; +} + +/** + * @internal + * Limited to Microsoft-internal use + */ +export function basicCall(input: ExampleInput): Promise { + if (!isSupported()) { + throw new Error(`Error code: ${ErrorCode.NOT_SUPPORTED_ON_PLATFORM}, message: ${errorNotSupportedOnPlatform}`); + } + return new Promise((resolve) => { + resolve({ status: `test successful - received: ${input.input}` }); + }); +} + +/** + * @internal + * Limited to Microsoft-internal use + */ +export function isEventSupported(): boolean { + return ensureInitialized(runtime) && runtime.supports.exampleFeature?.events ? true : false; +} + +/** + * @internal + * Limited to Microsoft-internal use + */ +export function registerEventHandler(handler: (data: ExampleEventData) => void): void { + if (!isEventSupported()) { + throw new Error(`Error code: ${ErrorCode.NOT_SUPPORTED_ON_PLATFORM}, message: ${errorNotSupportedOnPlatform}`); + } + window.addEventListener('exampleEvent', ((event: CustomEvent) => { + handler(event.detail); + }) as EventListener); +} + +/** + * @internal + * Limited to Microsoft-internal use + */ +export function raiseEvent(eventData: string): void { + if (!isEventSupported()) { + throw new Error(`Error code: ${ErrorCode.NOT_SUPPORTED_ON_PLATFORM}, message: ${errorNotSupportedOnPlatform}`); + } + window.dispatchEvent(new CustomEvent('exampleDirectEvent', { detail: eventData })); +} diff --git a/packages/teams-js/src/private/index.ts b/packages/teams-js/src/private/index.ts index f55e503c3e..e71550ea6e 100644 --- a/packages/teams-js/src/private/index.ts +++ b/packages/teams-js/src/private/index.ts @@ -39,3 +39,4 @@ export * as teams from './teams/teams'; export * as videoEffectsEx from './videoEffectsEx'; export * as hostEntity from './hostEntity/hostEntity'; export * as store from './store'; +export * as exampleFeature from './exampleFeature'; diff --git a/packages/teams-js/src/public/runtime.ts b/packages/teams-js/src/public/runtime.ts index 88c183b7ae..55ab29af37 100644 --- a/packages/teams-js/src/public/runtime.ts +++ b/packages/teams-js/src/public/runtime.ts @@ -78,6 +78,10 @@ interface IRuntimeV1 extends IBaseRuntime { readonly sharedFrame?: {}; }; readonly webStorage?: {}; + readonly exampleFeature?: { + readonly basicCall?: {}; + readonly events?: {}; + }; }; } @@ -142,6 +146,10 @@ interface IRuntimeV2 extends IBaseRuntime { readonly sharedFrame?: {}; }; readonly webStorage?: {}; + readonly exampleFeature?: { + readonly basicCall?: {}; + readonly events?: {}; + }; }; } @@ -214,6 +222,10 @@ interface IRuntimeV3 extends IBaseRuntime { readonly image?: {}; }; readonly webStorage?: {}; + readonly exampleFeature?: { + readonly basicCall?: {}; + readonly events?: {}; + }; }; } @@ -309,6 +321,10 @@ interface IRuntimeV4 extends IBaseRuntime { readonly image?: {}; }; readonly webStorage?: {}; + readonly exampleFeature?: { + readonly basicCall?: {}; + readonly events?: {}; + }; }; } // Constant used to set the runtime configuration @@ -365,7 +381,7 @@ export let runtime: Runtime | UninitializedRuntime = _uninitializedRuntime; * during initialization. */ export const versionAndPlatformAgnosticTeamsRuntimeConfig: Runtime = { - apiVersion: 4, + apiVersion: latestRuntimeApiVersion, isNAAChannelRecommended: false, hostVersionsInfo: teamsMinAdaptiveCardVersion, isLegacyTeams: true, @@ -385,6 +401,10 @@ export const versionAndPlatformAgnosticTeamsRuntimeConfig: Runtime = { }, update: {}, }, + exampleFeature: { + basicCall: {}, + events: {}, + }, interactive: {}, logs: {}, meetingRoom: {}, From 241e919880dad4f94c091acbb8550303f2eda927 Mon Sep 17 00:00:00 2001 From: alexneyman Date: Sun, 29 Dec 2024 17:59:19 -0500 Subject: [PATCH 2/2] make tests pass --- .../e2e-test-data/exampleFeature.json | 12 +++++---- .../src/components/ExampleFeatureAPIs.tsx | 4 +-- .../teams-js/src/private/exampleFeature.ts | 25 +++++-------------- packages/teams-js/src/public/runtime.ts | 25 ++++--------------- 4 files changed, 20 insertions(+), 46 deletions(-) diff --git a/apps/teams-test-app/e2e-test-data/exampleFeature.json b/apps/teams-test-app/e2e-test-data/exampleFeature.json index a31b1de503..cbfd1e790c 100644 --- a/apps/teams-test-app/e2e-test-data/exampleFeature.json +++ b/apps/teams-test-app/e2e-test-data/exampleFeature.json @@ -1,13 +1,15 @@ { - "name": "Example Feature", + "name": "ExampleFeature", "checkIsSupported": { - "version": ">2.0.0-beta.3" + "version": ">2.0.0-beta.3", + "toggleId": "exampleFeatureToggle", + "capabilityName": "ExampleFeature" }, "featureTests": [ { "feature": { "id": "exampleFeature", - "version": 1 + "version": 2 }, "testCases": [ { @@ -17,14 +19,14 @@ "inputValue": { "input": "test input" }, - "expectedTestAppValue": "{\"status\":\"test successful - received: test input\"}" + "expectedTestAppValue": "test successful - received: test input" }, { "title": "Basic Call - Missing Input", "type": "callResponse", "boxSelector": "#box_exampleFeature", "inputValue": {}, - "expectedTestAppValue": "Error: Input is required" + "expectedTestAppValue": "Error: Error: Input is required" }, { "title": "Register and Raise Event", diff --git a/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx b/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx index 59833861fe..c5fca29e5d 100644 --- a/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx +++ b/apps/teams-test-app/src/components/ExampleFeatureAPIs.tsx @@ -8,7 +8,7 @@ const CheckExampleFeatureCapability = (): React.ReactElement => ApiWithoutInput({ name: 'checkExampleFeatureCapability', title: 'Check Example Feature Capability', - onClick: async () => `Example Feature module ${exampleFeature.isSupported() ? 'is' : 'is not'} supported`, + onClick: async () => `ExampleFeature module ${exampleFeature.isSupported() ? 'is' : 'is not'} supported`, }); const BasicCall = (): React.ReactElement => @@ -83,7 +83,7 @@ const RegularTest = (): React.ReactElement => }); const ExampleFeatureAPIs = (): React.ReactElement => ( - + diff --git a/packages/teams-js/src/private/exampleFeature.ts b/packages/teams-js/src/private/exampleFeature.ts index f637ac42e1..28a160f641 100644 --- a/packages/teams-js/src/private/exampleFeature.ts +++ b/packages/teams-js/src/private/exampleFeature.ts @@ -1,6 +1,4 @@ import { ensureInitialized } from '../internal/internalAPIs'; -import { errorNotSupportedOnPlatform } from '../public/constants'; -import { ErrorCode } from '../public/interfaces'; import { runtime } from '../public/runtime'; /** @@ -41,7 +39,7 @@ export interface ExampleEventData { * Limited to Microsoft-internal use */ export function isSupported(): boolean { - return ensureInitialized(runtime) && runtime.supports.exampleFeature?.basicCall ? true : false; + return ensureInitialized(runtime) && runtime.supports.exampleFeature ? true : false; } /** @@ -49,30 +47,21 @@ export function isSupported(): boolean { * Limited to Microsoft-internal use */ export function basicCall(input: ExampleInput): Promise { - if (!isSupported()) { - throw new Error(`Error code: ${ErrorCode.NOT_SUPPORTED_ON_PLATFORM}, message: ${errorNotSupportedOnPlatform}`); + ensureInitialized(runtime); + if (!input.input) { + throw new Error('Input is required'); } return new Promise((resolve) => { resolve({ status: `test successful - received: ${input.input}` }); }); } -/** - * @internal - * Limited to Microsoft-internal use - */ -export function isEventSupported(): boolean { - return ensureInitialized(runtime) && runtime.supports.exampleFeature?.events ? true : false; -} - /** * @internal * Limited to Microsoft-internal use */ export function registerEventHandler(handler: (data: ExampleEventData) => void): void { - if (!isEventSupported()) { - throw new Error(`Error code: ${ErrorCode.NOT_SUPPORTED_ON_PLATFORM}, message: ${errorNotSupportedOnPlatform}`); - } + ensureInitialized(runtime); window.addEventListener('exampleEvent', ((event: CustomEvent) => { handler(event.detail); }) as EventListener); @@ -83,8 +72,6 @@ export function registerEventHandler(handler: (data: ExampleEventData) => void): * Limited to Microsoft-internal use */ export function raiseEvent(eventData: string): void { - if (!isEventSupported()) { - throw new Error(`Error code: ${ErrorCode.NOT_SUPPORTED_ON_PLATFORM}, message: ${errorNotSupportedOnPlatform}`); - } + ensureInitialized(runtime); window.dispatchEvent(new CustomEvent('exampleDirectEvent', { detail: eventData })); } diff --git a/packages/teams-js/src/public/runtime.ts b/packages/teams-js/src/public/runtime.ts index 55ab29af37..d45c17535c 100644 --- a/packages/teams-js/src/public/runtime.ts +++ b/packages/teams-js/src/public/runtime.ts @@ -78,10 +78,7 @@ interface IRuntimeV1 extends IBaseRuntime { readonly sharedFrame?: {}; }; readonly webStorage?: {}; - readonly exampleFeature?: { - readonly basicCall?: {}; - readonly events?: {}; - }; + readonly exampleFeature?: {}; }; } @@ -146,10 +143,7 @@ interface IRuntimeV2 extends IBaseRuntime { readonly sharedFrame?: {}; }; readonly webStorage?: {}; - readonly exampleFeature?: { - readonly basicCall?: {}; - readonly events?: {}; - }; + readonly exampleFeature?: {}; }; } @@ -222,10 +216,7 @@ interface IRuntimeV3 extends IBaseRuntime { readonly image?: {}; }; readonly webStorage?: {}; - readonly exampleFeature?: { - readonly basicCall?: {}; - readonly events?: {}; - }; + readonly exampleFeature?: {}; }; } @@ -321,10 +312,7 @@ interface IRuntimeV4 extends IBaseRuntime { readonly image?: {}; }; readonly webStorage?: {}; - readonly exampleFeature?: { - readonly basicCall?: {}; - readonly events?: {}; - }; + readonly exampleFeature?: {}; }; } // Constant used to set the runtime configuration @@ -401,10 +389,7 @@ export const versionAndPlatformAgnosticTeamsRuntimeConfig: Runtime = { }, update: {}, }, - exampleFeature: { - basicCall: {}, - events: {}, - }, + exampleFeature: {}, interactive: {}, logs: {}, meetingRoom: {},