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..cbfd1e790c --- /dev/null +++ b/apps/teams-test-app/e2e-test-data/exampleFeature.json @@ -0,0 +1,58 @@ +{ + "name": "ExampleFeature", + "checkIsSupported": { + "version": ">2.0.0-beta.3", + "toggleId": "exampleFeatureToggle", + "capabilityName": "ExampleFeature" + }, + "featureTests": [ + { + "feature": { + "id": "exampleFeature", + "version": 2 + }, + "testCases": [ + { + "title": "Basic Call - Success", + "type": "callResponse", + "boxSelector": "#box_exampleFeature", + "inputValue": { + "input": "test input" + }, + "expectedTestAppValue": "test successful - received: test input" + }, + { + "title": "Basic Call - Missing Input", + "type": "callResponse", + "boxSelector": "#box_exampleFeature", + "inputValue": {}, + "expectedTestAppValue": "Error: 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..c5fca29e5d --- /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 () => `ExampleFeature 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..28a160f641 --- /dev/null +++ b/packages/teams-js/src/private/exampleFeature.ts @@ -0,0 +1,77 @@ +import { ensureInitialized } from '../internal/internalAPIs'; +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 ? true : false; +} + +/** + * @internal + * Limited to Microsoft-internal use + */ +export function basicCall(input: ExampleInput): Promise { + 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 registerEventHandler(handler: (data: ExampleEventData) => void): void { + ensureInitialized(runtime); + window.addEventListener('exampleEvent', ((event: CustomEvent) => { + handler(event.detail); + }) as EventListener); +} + +/** + * @internal + * Limited to Microsoft-internal use + */ +export function raiseEvent(eventData: string): void { + ensureInitialized(runtime); + 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..d45c17535c 100644 --- a/packages/teams-js/src/public/runtime.ts +++ b/packages/teams-js/src/public/runtime.ts @@ -78,6 +78,7 @@ interface IRuntimeV1 extends IBaseRuntime { readonly sharedFrame?: {}; }; readonly webStorage?: {}; + readonly exampleFeature?: {}; }; } @@ -142,6 +143,7 @@ interface IRuntimeV2 extends IBaseRuntime { readonly sharedFrame?: {}; }; readonly webStorage?: {}; + readonly exampleFeature?: {}; }; } @@ -214,6 +216,7 @@ interface IRuntimeV3 extends IBaseRuntime { readonly image?: {}; }; readonly webStorage?: {}; + readonly exampleFeature?: {}; }; } @@ -309,6 +312,7 @@ interface IRuntimeV4 extends IBaseRuntime { readonly image?: {}; }; readonly webStorage?: {}; + readonly exampleFeature?: {}; }; } // Constant used to set the runtime configuration @@ -365,7 +369,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 +389,7 @@ export const versionAndPlatformAgnosticTeamsRuntimeConfig: Runtime = { }, update: {}, }, + exampleFeature: {}, interactive: {}, logs: {}, meetingRoom: {},