diff --git a/.prettierignore b/.prettierignore index 73ec0b35e..cd3688288 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ # ignore folders website dist +src/app-directory/*/target diff --git a/docs/api/overview.md b/docs/api/overview.md index 8ca747da9..78aa4e028 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -53,24 +53,26 @@ The [`@finos/fdc3` npm package](https://www.npmjs.com/package/@finos/fdc3) provi ```ts import * as fdc3 from '@finos/fdc3' -const listener = fdc3.addIntentListener('ViewAnalysis', context => { - // do something +await fdc3.raiseIntent('ViewAnalysis', { + type: 'fdc3.instrument', + id: { ticker: 'AAPL' } }) ``` -Alternatively you can also import individual operations directly: +It also includes a helper function you can use to wait for FDC3 to become available: ```ts -import { raiseIntent } from '@finos/fdc3' +import { fdc3Ready, addIntentListener } from '@finos/fdc3' -await raiseIntent('ViewAnalysis', { - type: 'fdc3.instrument', - id: { ticker: 'AAPL' } +await fdc3Ready(); + +const listener = addIntentListener('ViewAnalysis', instrument => { + // handle intent }) ``` -The npm package will take care of checking for the existence of the global `fdc3` object, and wait for the `fdc3Ready` event, or throw an error if FDC3 is not supported. - +#### See also +* [`fdc3Ready() Function`](ref/Globals#fdc3ready-function) diff --git a/docs/api/ref/Globals.md b/docs/api/ref/Globals.md index 87577ad10..ec9e4c605 100644 --- a/docs/api/ref/Globals.md +++ b/docs/api/ref/Globals.md @@ -35,6 +35,33 @@ function fdc3Action() { if (window.fdc3) { fdc3Action(); } else { - window.addEventListener("fdc3Ready", fdc3Action); + window.addEventListener('fdc3Ready', fdc3Action); } ``` + +## `fdc3Ready()` Function + +If you are using the `@finos/fdc3` NPM package, it includes a handy wrapper function that will check for the existence of `window.fdc3` and wait on the `fdc3Ready` event for you. + +It returns a promise that will resolve immediately if the `window.fdc3` global is already defined, or reject with an error if the `fdc3Ready` event doesn't fire after a specified timeout period (default: 5 seconds). + +### Example + +```ts +import { fdc3Ready, broadcast } from '@finos/fdc3' + +async function fdc3Action() { + try { + await fdc3Ready(1000); // wait for (at most) 1 second + broadcast({ + type: 'fdc3.instrument', + id: { ticker: 'AAPL' } + }) + } catch (error) { + // handle error + } +} +``` + + + diff --git a/package.json b/package.json index a0527c8f7..fcee13a30 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "singleQuote": true, "arrowParens": "avoid", "trailingComma": "es5", - "endOfLine": "auto" + "endOfLine": "auto", + "printWidth": 120 }, "resolutions": { "node-fetch": "^2.6.1", @@ -47,6 +48,7 @@ }, "devDependencies": { "husky": "^4.3.0", + "jest-mock-extended": "^1.0.13", "quicktype": "^15.0.258", "tsdx": "^0.14.1", "tslib": "^2.0.1", diff --git a/src/api/Channel.ts b/src/api/Channel.ts index 87ed43236..86a96d14a 100644 --- a/src/api/Channel.ts +++ b/src/api/Channel.ts @@ -68,8 +68,5 @@ export interface Channel { /** * Adds a listener for incoming contexts of the specified context type whenever a broadcast happens on this channel. */ - addContextListener( - contextType: string | null, - handler: ContextHandler - ): Listener; + addContextListener(contextType: string | null, handler: ContextHandler): Listener; } diff --git a/src/api/DesktopAgent.ts b/src/api/DesktopAgent.ts index c07694491..5decc8c56 100644 --- a/src/api/DesktopAgent.ts +++ b/src/api/DesktopAgent.ts @@ -124,11 +124,7 @@ export interface DesktopAgent { * await fdc3.raiseIntent("StartChat", context, appMetadata); * ``` */ - raiseIntent( - intent: string, - context: Context, - app?: TargetApp - ): Promise; + raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; /** * Raises a context to the desktop agent to resolve with one of the possible Intents for that context. @@ -136,10 +132,7 @@ export interface DesktopAgent { * await fdc3.raiseIntentForContext(context); * ``` */ - raiseIntentForContext( - context: Context, - app?: TargetApp - ): Promise; + raiseIntentForContext(context: Context, app?: TargetApp): Promise; /** * Adds a listener for incoming Intents from the Agent. @@ -155,10 +148,7 @@ export interface DesktopAgent { /** * Adds a listener for the broadcast of a specific type of context object. */ - addContextListener( - contextType: string | null, - handler: ContextHandler - ): Listener; + addContextListener(contextType: string | null, handler: ContextHandler): Listener; /** * Retrieves a list of the System channels available for the app to join diff --git a/src/api/Methods.ts b/src/api/Methods.ts index 6a08ae2d5..d405aab3f 100644 --- a/src/api/Methods.ts +++ b/src/api/Methods.ts @@ -1,113 +1,105 @@ -import { - AppIntent, - Channel, - Context, - ContextHandler, - IntentResolution, - Listener, - ImplementationMetadata, -} from '..'; +import { AppIntent, Channel, Context, ContextHandler, IntentResolution, Listener, ImplementationMetadata } from '..'; import { TargetApp } from './Types'; -const unavailableError = new Error( - 'FDC3 DesktopAgent not available at `window.fdc3`.' -); +const DEFAULT_TIMEOUT = 5000; -const rejectIfNoGlobal = (f: () => Promise) => { - return window.fdc3 ? f() : Promise.reject(unavailableError); -}; +const UnavailableError = new Error('FDC3 DesktopAgent not available at `window.fdc3`.'); +const TimeoutError = new Error('Timed out waiting for `fdc3Ready` event.'); +const UnexpectedError = new Error('`fdc3Ready` event fired, but `window.fdc3` not set to DesktopAgent.'); + +function rejectIfNoGlobal(f: () => Promise) { + return window.fdc3 ? f() : Promise.reject(UnavailableError); +} -const throwIfNoGlobal = (f: () => any) => { +function throwIfNoGlobal(f: () => any) { if (!window.fdc3) { - throw unavailableError; + throw UnavailableError; } return f(); +} + +export const fdc3Ready = async (waitForMs = DEFAULT_TIMEOUT): Promise => { + return new Promise((resolve, reject) => { + // if the global is already available resolve immediately + if (window.fdc3) { + resolve(); + } else { + // if its not available setup a timeout to return a rejected promise + const timeout = setTimeout(() => (window.fdc3 ? resolve() : reject(TimeoutError)), waitForMs); + // listen for the fdc3Ready event + window.addEventListener( + 'fdc3Ready', + () => { + clearTimeout(timeout); + window.fdc3 ? resolve() : reject(UnexpectedError); + }, + { once: true } + ); + } + }); }; -export const open: (app: TargetApp, context?: Context) => Promise = ( - app, - context -) => { +export function open(app: TargetApp, context?: Context): Promise { return rejectIfNoGlobal(() => window.fdc3.open(app, context)); -}; +} -export const findIntent: ( - intent: string, - context?: Context -) => Promise = (intent, context) => { +export function findIntent(intent: string, context?: Context): Promise { return rejectIfNoGlobal(() => window.fdc3.findIntent(intent, context)); -}; +} -export const findIntentsByContext: ( - context: Context -) => Promise> = context => { +export function findIntentsByContext(context: Context): Promise { return rejectIfNoGlobal(() => window.fdc3.findIntentsByContext(context)); -}; +} -export const broadcast: (context: Context) => void = context => { +export function broadcast(context: Context): void { throwIfNoGlobal(() => window.fdc3.broadcast(context)); -}; +} -export const raiseIntent: ( - intent: string, - context: Context, - app?: TargetApp -) => Promise = (intent, context, app) => { +export function raiseIntent(intent: string, context: Context, app?: TargetApp): Promise { return rejectIfNoGlobal(() => window.fdc3.raiseIntent(intent, context, app)); -}; +} -export const raiseIntentForContext: ( - context: Context, - app?: TargetApp -) => Promise = (context, app) => { - return rejectIfNoGlobal(() => - window.fdc3.raiseIntentForContext(context, app) - ); -}; +export function raiseIntentForContext(context: Context, app?: TargetApp): Promise { + return rejectIfNoGlobal(() => window.fdc3.raiseIntentForContext(context, app)); +} -export const addIntentListener: ( - intent: string, - handler: ContextHandler -) => Listener = (intent, handler) => { +export function addIntentListener(intent: string, handler: ContextHandler): Listener { return throwIfNoGlobal(() => window.fdc3.addIntentListener(intent, handler)); -}; +} -export const addContextListener: ( - contextTypeOrHandler: string | ContextHandler, - handler?: ContextHandler -) => Listener = (a, b) => { - if (typeof a !== 'function') { +export function addContextListener(contextTypeOrHandler: string | ContextHandler, handler?: ContextHandler): Listener { + if (typeof contextTypeOrHandler !== 'function') { return throwIfNoGlobal(() => - window.fdc3.addContextListener(a as string, b as ContextHandler) + window.fdc3.addContextListener(contextTypeOrHandler as string, handler as ContextHandler) ); } else { - return throwIfNoGlobal(() => - window.fdc3.addContextListener(a as ContextHandler) - ); + return throwIfNoGlobal(() => window.fdc3.addContextListener(contextTypeOrHandler as ContextHandler)); } -}; +} -export const getSystemChannels: () => Promise> = () => { +export function getSystemChannels(): Promise { return rejectIfNoGlobal(() => window.fdc3.getSystemChannels()); -}; +} -export const joinChannel: (channelId: string) => Promise = channelId => { +export function joinChannel(channelId: string): Promise { return rejectIfNoGlobal(() => window.fdc3.joinChannel(channelId)); -}; +} -export const getOrCreateChannel: ( - channelId: string -) => Promise = channelId => { +export function getOrCreateChannel(channelId: string): Promise { return rejectIfNoGlobal(() => window.fdc3.getOrCreateChannel(channelId)); -}; +} -export const getCurrentChannel: () => Promise = () => { +export function getCurrentChannel(): Promise { return rejectIfNoGlobal(() => window.fdc3.getCurrentChannel()); -}; +} -export const leaveCurrentChannel: () => Promise = () => { +export function leaveCurrentChannel(): Promise { return rejectIfNoGlobal(() => window.fdc3.leaveCurrentChannel()); -}; +} + +export function getInfo(): ImplementationMetadata { + return throwIfNoGlobal(() => window.fdc3.getInfo()); +} /** * Compare numeric semver version number strings (in the form `1.2.3`). @@ -119,19 +111,12 @@ export const leaveCurrentChannel: () => Promise = () => { * @param a * @param b */ -export const compareVersionNumbers: (a: string, b: string) => number | null = ( - a, - b -) => { +export const compareVersionNumbers: (a: string, b: string) => number | null = (a, b) => { try { let aVerArr = a.split('.').map(Number); let bVerArr = b.split('.').map(Number); - for ( - let index = 0; - index < Math.max(aVerArr.length, bVerArr.length); - index++ - ) { - /* If one version number has more digits and the other does not, and they are otherwise equal, + for (let index = 0; index < Math.max(aVerArr.length, bVerArr.length); index++) { + /* If one version number has more digits and the other does not, and they are otherwise equal, assume the longer is greater. E.g. 1.1.1 > 1.1 */ if (index === aVerArr.length || aVerArr[index] < bVerArr[index]) { return -1; @@ -155,10 +140,10 @@ export const compareVersionNumbers: (a: string, b: string) => number | null = ( * @param metadata * @param version */ -export const versionIsAtLeast: ( - metadata: ImplementationMetadata, - version: string -) => boolean | null = (metadata, version) => { +export const versionIsAtLeast: (metadata: ImplementationMetadata, version: string) => boolean | null = ( + metadata, + version +) => { let comparison = compareVersionNumbers(metadata.fdc3Version, version); return comparison === null ? null : comparison >= 0 ? true : false; }; diff --git a/src/context/ContextTypes.ts b/src/context/ContextTypes.ts index eecb30cc8..70b4f1d78 100644 --- a/src/context/ContextTypes.ts +++ b/src/context/ContextTypes.ts @@ -180,15 +180,9 @@ export class Convert { function invalidValue(typ: any, val: any, key: any = ''): never { if (key) { - throw Error( - `Invalid value for key "${key}". Expected type ${JSON.stringify( - typ - )} but got ${JSON.stringify(val)}` - ); + throw Error(`Invalid value for key "${key}". Expected type ${JSON.stringify(typ)} but got ${JSON.stringify(val)}`); } - throw Error( - `Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}` - ); + throw Error(`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`); } function jsonToJSProps(typ: any): any { @@ -249,20 +243,14 @@ function transform(val: any, typ: any, getProps: any, key: any = ''): any { return d; } - function transformObject( - props: { [k: string]: any }, - additional: any, - val: any - ): any { + function transformObject(props: { [k: string]: any }, additional: any, val: any): any { if (val === null || typeof val !== 'object' || Array.isArray(val)) { return invalidValue('object', val); } const result: any = {}; Object.getOwnPropertyNames(props).forEach(key => { const prop = props[key]; - const v = Object.prototype.hasOwnProperty.call(val, key) - ? val[key] - : undefined; + const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined; result[prop.key] = transform(v, prop.typ, getProps, prop.key); }); Object.getOwnPropertyNames(val).forEach(key => { diff --git a/test/ContextTypes.test.ts b/test/ContextTypes.test.ts index cfb60ecb1..0c274de02 100644 --- a/test/ContextTypes.test.ts +++ b/test/ContextTypes.test.ts @@ -15,8 +15,6 @@ describe('Context types', () => { }); it('Convert contact to json', () => { - expect(JSON.parse(Convert.contactToJson(contact))).toEqual( - JSON.parse(json) - ); + expect(JSON.parse(Convert.contactToJson(contact))).toEqual(JSON.parse(json)); }); }); diff --git a/test/Methods.test.ts b/test/Methods.test.ts index 98bacb4b4..f6d8acdaa 100644 --- a/test/Methods.test.ts +++ b/test/Methods.test.ts @@ -1,36 +1,31 @@ +import { mock } from 'jest-mock-extended'; import { addContextListener, addIntentListener, broadcast, + compareVersionNumbers, ContextHandler, ContextTypes, DesktopAgent, + fdc3Ready, findIntent, findIntentsByContext, getCurrentChannel, + getInfo, getOrCreateChannel, getSystemChannels, + ImplementationMetadata, joinChannel, leaveCurrentChannel, open, raiseIntent, raiseIntentForContext, - ImplementationMetadata, + versionIsAtLeast, } from '../src'; -import * as methods from '../src/api/Methods'; -declare global { - namespace jest { - interface Matchers { - toRejectWithUnavailableError: () => CustomMatcherResult; - toThrowUnavailableError: () => CustomMatcherResult; - } - } -} - -const UnavailableError = new Error( - 'FDC3 DesktopAgent not available at `window.fdc3`.' -); +const UnavailableError = new Error('FDC3 DesktopAgent not available at `window.fdc3`.'); +const TimeoutError = new Error('Timed out waiting for `fdc3Ready` event.'); +const UnexpectedError = new Error('`fdc3Ready` event fired, but `window.fdc3` not set to DesktopAgent.'); const ContactContext = { type: ContextTypes.Contact, @@ -50,172 +45,136 @@ expect.extend({ describe('test ES6 module', () => { describe('without `window.fdc3` global', () => { - let originalFdc3: DesktopAgent; - beforeAll(() => { - originalFdc3 = window.fdc3; - window.fdc3 = (undefined as unknown) as DesktopAgent; + test('open should reject', async () => { + await expect(open(expect.any(String))).rejects.toEqual(UnavailableError); }); - afterAll(() => { - window.fdc3 = originalFdc3; + test('findIntent should reject', async () => { + await expect(findIntent(expect.any(String))).rejects.toEqual(UnavailableError); }); - test('open should reject', () => { - expect(open(expect.any(String))).toRejectWithUnavailableError(); + test('findIntentsByContext should reject', async () => { + await expect(findIntentsByContext(expect.any(Object))).rejects.toEqual(UnavailableError); }); - test('findIntent should reject', () => { - expect(findIntent(expect.any(String))).toRejectWithUnavailableError(); + test('broadcast should throw', async () => { + expect(() => broadcast(expect.any(Object))).toThrowError(UnavailableError); }); - test('findIntentsByContext should reject', () => { - expect( - findIntentsByContext(expect.any(Object)) - ).toRejectWithUnavailableError(); + test('raiseIntent should reject', async () => { + await expect(raiseIntent(expect.any(String), expect.any(Object))).rejects.toEqual(UnavailableError); }); - test('broadcast should throw', () => { - expect(() => broadcast(expect.any(Object))).toThrowUnavailableError(); + test('raiseIntentForContext should reject', async () => { + await expect(raiseIntentForContext(expect.any(Object))).rejects.toEqual(UnavailableError); }); - test('raiseIntent should reject', () => { - expect( - raiseIntent(expect.any(String), expect.any(Object)) - ).toRejectWithUnavailableError(); + test('addIntentListener should throw', () => { + expect(() => addIntentListener(expect.any(String), expect.any(Function))).toThrowError(UnavailableError); }); - test('raiseIntentForContext should reject', () => { - expect( - raiseIntentForContext(expect.any(Object)) - ).toRejectWithUnavailableError(); - }); + test('addContextListener should throw', () => { + expect(() => addContextListener(expect.any(Object))).toThrowError(UnavailableError); - test('addIntentListener should throw', () => { - expect(() => - addIntentListener(expect.any(String), expect.any(Function)) - ).toThrowUnavailableError(); + expect(() => addContextListener(expect.any(String), expect.any(Object))).toThrowError(UnavailableError); }); - test('addContextListener should throw', () => { - expect(() => - addContextListener(expect.any(Object)) - ).toThrowUnavailableError(); + test('getSystemChannels should reject', async () => { + await expect(getSystemChannels()).rejects.toEqual(UnavailableError); + }); - expect(() => - addContextListener(expect.any(String), expect.any(Object)) - ).toThrowUnavailableError(); + test('joinChannel should reject', async () => { + await expect(joinChannel(expect.any(String))).rejects.toEqual(UnavailableError); }); - test('getSystemChannels should reject', () => { - expect(getSystemChannels()).toRejectWithUnavailableError(); + test('getOrCreateChannel should reject', async () => { + await expect(getOrCreateChannel(expect.any(String))).rejects.toEqual(UnavailableError); }); - test('joinChannel should reject', () => { - expect(joinChannel(expect.any(String))).toRejectWithUnavailableError(); + test('getCurrentChannel should reject', async () => { + await expect(getCurrentChannel()).rejects.toEqual(UnavailableError); }); - test('getOrCreateChannel should reject', () => { - expect( - getOrCreateChannel(expect.any(String)) - ).toRejectWithUnavailableError(); + test('leaveCurrentChannel should reject', async () => { + await expect(leaveCurrentChannel()).rejects.toEqual(UnavailableError); }); - test('getCurrentChannel should reject', () => { - expect(getCurrentChannel()).toRejectWithUnavailableError(); + test('getInfo should throw', () => { + expect(() => getInfo()).toThrowError(UnavailableError); }); }); describe('with `window.fdc3` global', () => { - const mocks: Map> = new Map(); - const getMock: (name: string) => jest.Mock = name => { - const mock = mocks.get(name); - if (!mock) { - throw new Error('No mock named ' + name); - } - return mock; - }; - beforeAll(() => { - const fdc3 = {}; - - for (const method of Object.keys(methods)) { - const mock = jest.fn(); - mocks.set(method, mock); - Object.defineProperty(fdc3, method, { value: mock }); - } + window.fdc3 = mock(); + }); - window.fdc3 = fdc3 as DesktopAgent; + afterAll(() => { + window.fdc3 = (undefined as unknown) as DesktopAgent; }); - it('open should delegate to window.fdc3.open', () => { - const app = 'MyApp'; + test('open should delegate to window.fdc3.open', async () => { + const target = 'MyApp'; - open(app, ContactContext); + await open(target, ContactContext); - const mock = getMock('open'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([app, ContactContext]); + expect(window.fdc3.open).toHaveBeenCalledTimes(1); + expect(window.fdc3.open).toHaveBeenCalledWith(target, ContactContext); }); - it('findIntent should delegate to window.fdc3.findIntent', () => { - const intent = 'ViewChat'; + test('findIntent should delegate to window.fdc3.findIntent', async () => { + const intent = 'ViewChart'; - findIntent(intent, ContactContext); + await findIntent(intent, ContactContext); - const mock = getMock('findIntent'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([intent, ContactContext]); + expect(window.fdc3.findIntent).toHaveBeenCalledTimes(1); + expect(window.fdc3.findIntent).toHaveBeenCalledWith(intent, ContactContext); }); - it('findIntentsByContext should delegate to window.fdc3.findIntentsByContext', () => { - findIntentsByContext(ContactContext); + test('findIntentsByContext should delegate to window.fdc3.findIntentsByContext', async () => { + await findIntentsByContext(ContactContext); - const mock = getMock('findIntentsByContext'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([ContactContext]); + expect(window.fdc3.findIntentsByContext).toHaveBeenCalledTimes(1); + expect(window.fdc3.findIntentsByContext).toHaveBeenCalledWith(ContactContext); }); - it('broadcast should delegate to window.fdc3.broadcast', () => { + test('broadcast should delegate to window.fdc3.broadcast', () => { broadcast(ContactContext); - const mock = getMock('broadcast'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([ContactContext]); + expect(window.fdc3.broadcast).toHaveBeenCalledTimes(1); + expect(window.fdc3.broadcast).toHaveBeenCalledWith(ContactContext); }); - it('raiseIntent should delegate to window.fdc3.raiseIntent', () => { - const intent = 'ViewChat'; + test('raiseIntent should delegate to window.fdc3.raiseIntent', async () => { + const intent = 'ViewChart'; const target = 'MyApp'; - raiseIntent(intent, ContactContext, target); + await raiseIntent(intent, ContactContext, target); - const mock = getMock('raiseIntent'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([intent, ContactContext, target]); + expect(window.fdc3.raiseIntent).toHaveBeenCalledTimes(1); + expect(window.fdc3.raiseIntent).toHaveBeenCalledWith(intent, ContactContext, target); }); - it('raiseIntentForContext should delegate to window.fdc3.raiseIntentForContext', () => { + test('raiseIntentForContext should delegate to window.fdc3.raiseIntentForContext', async () => { const app = 'MyApp'; - raiseIntentForContext(ContactContext, app); + await raiseIntentForContext(ContactContext, app); - const mock = getMock('raiseIntentForContext'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([ContactContext, app]); + expect(window.fdc3.raiseIntentForContext).toHaveBeenCalledTimes(1); + expect(window.fdc3.raiseIntentForContext).toHaveBeenCalledWith(ContactContext, app); }); - it('addIntentListener should delegate to window.fdc3.addIntentListener', () => { - const intent = 'ViewChat'; + test('addIntentListener should delegate to window.fdc3.addIntentListener', () => { + const intent = 'ViewChart'; const handler: ContextHandler = _ => {}; addIntentListener(intent, handler); - const mock = getMock('addIntentListener'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([intent, handler]); + expect(window.fdc3.addIntentListener).toHaveBeenCalledTimes(1); + expect(window.fdc3.addIntentListener).toHaveBeenCalledWith(intent, handler); }); - it('addContextListener should delegate to window.fdc3.addContextListener', () => { + test('addContextListener should delegate to window.fdc3.addContextListener', () => { const type = 'fdc3.instrument'; const handler1: ContextHandler = _ => {}; const handler2: ContextHandler = _ => {}; @@ -223,67 +182,152 @@ describe('test ES6 module', () => { addContextListener(type, handler1); addContextListener(handler2); - const mock = getMock('addContextListener'); - expect(mock.mock.calls.length).toBe(2); - expect(mock.mock.calls[0]).toEqual([type, handler1]); - expect(mock.mock.calls[1]).toEqual([handler2]); + expect(window.fdc3.addContextListener).toHaveBeenCalledTimes(2); + expect(window.fdc3.addContextListener).toHaveBeenNthCalledWith(1, type, handler1); + expect(window.fdc3.addContextListener).toHaveBeenNthCalledWith(2, handler2); }); - it('getSystemChannels should delegate to window.fdc3.getSystemChannels', () => { - getSystemChannels(); + test('getSystemChannels should delegate to window.fdc3.getSystemChannels', async () => { + await getSystemChannels(); - const mock = getMock('getSystemChannels'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([]); + expect(window.fdc3.getSystemChannels).toHaveBeenCalledTimes(1); + expect(window.fdc3.getSystemChannels).toHaveBeenCalledWith(); }); - it('joinChannel should delegate to window.fdc3.joinChannel', () => { + test('joinChannel should delegate to window.fdc3.joinChannel', async () => { const channelId = 'channel'; - joinChannel(channelId); + await joinChannel(channelId); - const mock = getMock('joinChannel'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([channelId]); + expect(window.fdc3.joinChannel).toHaveBeenCalledTimes(1); + expect(window.fdc3.joinChannel).toHaveBeenCalledWith(channelId); }); - it('getOrCreateChannel should delegate to window.fdc3.getOrCreateChannel', () => { + test('getOrCreateChannel should delegate to window.fdc3.getOrCreateChannel', async () => { const channelId = 'channel'; - getOrCreateChannel(channelId); + await getOrCreateChannel(channelId); + + expect(window.fdc3.getOrCreateChannel).toHaveBeenCalledTimes(1); + expect(window.fdc3.getOrCreateChannel).toHaveBeenCalledWith(channelId); + }); + + test('getCurrentChannel should delegate to window.fdc3.getCurrentChannel', async () => { + await getCurrentChannel(); + + expect(window.fdc3.getCurrentChannel).toHaveBeenCalledTimes(1); + expect(window.fdc3.getCurrentChannel).toHaveBeenCalledWith(); + }); + + test('leaveCurrentChannel should delegate to window.fdc3.leaveCurrentChannel', async () => { + await leaveCurrentChannel(); - const mock = getMock('getOrCreateChannel'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([channelId]); + expect(window.fdc3.leaveCurrentChannel).toHaveBeenCalledTimes(1); + expect(window.fdc3.leaveCurrentChannel).toHaveBeenCalledWith(); }); - it('getCurrentChannel should delegate to window.fdc3.getCurrentChannel', () => { - getCurrentChannel(); + test('getInfo should delegate to window.fdc3.getInfo', () => { + getInfo(); - const mock = getMock('getCurrentChannel'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([]); + expect(window.fdc3.getInfo).toHaveBeenCalledTimes(1); + expect(window.fdc3.getInfo).toHaveBeenCalledWith(); }); + }); + + describe('fdc3Ready', () => { + let eventListeners: any; + + beforeEach(() => { + jest.useFakeTimers(); + + eventListeners = {}; + + window.addEventListener = jest.fn((event, callback) => { + eventListeners[event] = callback; + }); + }); + + afterEach(() => { + window.fdc3 = (undefined as unknown) as DesktopAgent; + }); + + test('resolves immediately if `window.fdc3` is already defined', async () => { + // set fdc3 object and call fdc3Ready + window.fdc3 = mock(); + const promise = fdc3Ready(); + + expect(setTimeout).not.toHaveBeenCalled(); + expect(clearTimeout).not.toHaveBeenCalled(); + expect(eventListeners).not.toHaveProperty('fdc3Ready'); + await expect(promise).resolves.toBe(undefined); + }); + + test('waits for specified milliseconds', async () => { + const waitForMs = 1000; + + const promise = fdc3Ready(waitForMs); + + expect(setTimeout).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), waitForMs); + + jest.advanceTimersByTime(waitForMs); + + await expect(promise).rejects.toEqual(TimeoutError); + }); + + test('waits for 5000 milliseconds by default', async () => { + const defaultWaitForMs = 5000; + + const promise = fdc3Ready(); + + expect(setTimeout).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), defaultWaitForMs); + + jest.advanceTimersByTime(defaultWaitForMs); + + await expect(promise).rejects.toEqual(TimeoutError); + }); + + test('`fdc3Ready` event cancels timeout and rejects if `window.fdc3` is not defined', () => { + const promise = fdc3Ready(); + + expect(setTimeout).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 5000); + expect(eventListeners).toHaveProperty('fdc3Ready'); + + // trigger fdc3Ready event without setting fdc3 object + eventListeners['fdc3Ready'](); + + expect(clearTimeout).toHaveBeenCalledTimes(1); + return expect(promise).rejects.toEqual(UnexpectedError); + }); + + test('`fdc3Ready` event cancels timeout and resolves if `window.fdc3` is defined', async () => { + const promise = fdc3Ready(); + + expect(setTimeout).toHaveBeenCalledTimes(1); + expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 5000); + expect(eventListeners).toHaveProperty('fdc3Ready'); - it('leaveCurrentChannel should delegate to window.fdc3.leaveCurrentChannel', () => { - leaveCurrentChannel(); + // set fdc3 object and trigger fdc3 ready event + window.fdc3 = mock(); + eventListeners['fdc3Ready'](); - const mock = getMock('leaveCurrentChannel'); - expect(mock.mock.calls.length).toBe(1); - expect(mock.mock.calls[0]).toEqual([]); + expect(clearTimeout).toHaveBeenCalledTimes(1); + await expect(promise).resolves.toBe(undefined); }); }); }); describe('test version comparison functions', () => { test('compareVersionNumbers', () => { - expect(methods.compareVersionNumbers('1.1', '1.2')).toBe(-1); - expect(methods.compareVersionNumbers('1.2', '1.1')).toBe(1); - expect(methods.compareVersionNumbers('1.2', '1.2')).toBe(0); - expect(methods.compareVersionNumbers('1.1.1', '1.2')).toBe(-1); - expect(methods.compareVersionNumbers('1.1.1', '1.1')).toBe(1); - expect(methods.compareVersionNumbers('1.1', '1.1.1')).toBe(-1); - expect(methods.compareVersionNumbers('1.1.1', '1.1.1')).toBe(0); + expect(compareVersionNumbers('1.1', '1.2')).toBe(-1); + expect(compareVersionNumbers('1.2', '1.1')).toBe(1); + expect(compareVersionNumbers('1.2', '1.2')).toBe(0); + expect(compareVersionNumbers('1.1.1', '1.2')).toBe(-1); + expect(compareVersionNumbers('1.1.1', '1.1')).toBe(1); + expect(compareVersionNumbers('1.1', '1.1.1')).toBe(-1); + expect(compareVersionNumbers('1.1.1', '1.1.1')).toBe(0); }); test('versionIsAtLeast', () => { @@ -291,18 +335,18 @@ describe('test version comparison functions', () => { fdc3Version: '1.2', provider: 'test', }; - expect(methods.versionIsAtLeast(metaOneTwo, '1.1')).toBe(true); - expect(methods.versionIsAtLeast(metaOneTwo, '1.2')).toBe(true); - expect(methods.versionIsAtLeast(metaOneTwo, '1.2.1')).toBe(false); - expect(methods.versionIsAtLeast(metaOneTwo, '2.0')).toBe(false); + expect(versionIsAtLeast(metaOneTwo, '1.1')).toBe(true); + expect(versionIsAtLeast(metaOneTwo, '1.2')).toBe(true); + expect(versionIsAtLeast(metaOneTwo, '1.2.1')).toBe(false); + expect(versionIsAtLeast(metaOneTwo, '2.0')).toBe(false); const metaOneTwoOne: ImplementationMetadata = { fdc3Version: '1.2.1', provider: 'test', }; - expect(methods.versionIsAtLeast(metaOneTwoOne, '1.1')).toBe(true); - expect(methods.versionIsAtLeast(metaOneTwoOne, '1.2')).toBe(true); - expect(methods.versionIsAtLeast(metaOneTwoOne, '1.2.1')).toBe(true); - expect(methods.versionIsAtLeast(metaOneTwoOne, '2.0')).toBe(false); + expect(versionIsAtLeast(metaOneTwoOne, '1.1')).toBe(true); + expect(versionIsAtLeast(metaOneTwoOne, '1.2')).toBe(true); + expect(versionIsAtLeast(metaOneTwoOne, '1.2.1')).toBe(true); + expect(versionIsAtLeast(metaOneTwoOne, '2.0')).toBe(false); }); }); diff --git a/yarn.lock b/yarn.lock index 768ed45d9..713656443 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3812,6 +3812,13 @@ jest-message-util@^25.5.0: slash "^3.0.0" stack-utils "^1.0.1" +jest-mock-extended@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-1.0.13.tgz#07d7b58ea45a4bad8d95ff8ae30ca8f6171c9289" + integrity sha512-fs621RgUK9Z6frJWDA75Gb9Vpm1d9HbpuAWjIu12hZTNls+VVdlJl/43xrqz70CYUbJPnfkiTfLwue9mkKC1ww== + dependencies: + ts-essentials "^4.0.0" + jest-mock@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.5.0.tgz#a91a54dabd14e37ecd61665d6b6e06360a55387a" @@ -6223,6 +6230,11 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +ts-essentials@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-4.0.0.tgz#506c42b270bbd0465574b90416533175b09205ab" + integrity sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ== + ts-jest@^25.3.1: version "25.5.1" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7"