From c163902ac539e61d3cee86233b69446d31bb3002 Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Wed, 28 Aug 2024 13:55:28 +0100 Subject: [PATCH 01/11] Channel Selector and intent resolver working --- .../packages/client/package.json | 8 +- .../DefaultDesktopAgentChannelSelector.ts | 147 --------------- .../DefaultDesktopAgentIntentResolver.ts | 114 ------------ .../intent-resolution/NullIIntentResolver.ts | 15 -- .../client/src/messaging/message-port.ts | 22 ++- .../client/src/ui/AbstractUIComponent.ts | 115 ++++++++++++ .../ui/DefaultDesktopAgentChannelSelector.ts | 56 ++++++ .../ui/DefaultDesktopAgentIntentResolver.ts | 56 ++++++ .../NullChannelSelector.ts | 4 +- .../client/src/ui/NullIIntentResolver.ts | 14 ++ .../packages/da-proxy/package.json | 6 +- .../src/channels/DefaultChannelSupport.ts | 7 + .../src/intents/DefaultIntentSupport.ts | 70 +++++--- .../test/features/raise-intents.feature | 71 ++++++++ .../test/step-definitions/channels.steps.ts | 3 + .../da-proxy/test/support/TestMessaging.ts | 5 +- .../test/support/responses/RaiseIntent.ts | 95 ++++++++-- .../responses/RaiseIntentForContext.ts | 147 +++++++++++++++ .../packages/da-server/package.json | 6 +- .../packages/demo/package.json | 8 +- .../packages/demo/src/client/apps/app7.ts | 19 ++ .../demo/src/client/da/dummy-desktop-agent.ts | 42 +++-- .../packages/demo/src/client/da/embed.ts | 1 - .../demo/src/client/ui/channel-selector.ts | 43 ++++- .../demo/src/client/ui/intent-resolver.ts | 58 ++++-- .../packages/demo/static/app7/index.html | 15 ++ .../packages/demo/static/da/appd.json | 14 ++ .../packages/fdc3-common/package.json | 2 +- .../packages/fdc3-common/src/BrowserTypes.ts | 168 +++++++++++++++-- .../fdc3-common/src/ChannelSelector.ts | 24 +++ .../fdc3-common/src/IntentResolver.ts | 25 +++ .../packages/fdc3-common/src/index.ts | 169 +----------------- .../packages/testing/package.json | 4 +- .../packages/testing/src/agent/index.ts | 20 ++- 34 files changed, 1016 insertions(+), 557 deletions(-) delete mode 100644 fdc3-for-web-implementation/packages/client/src/channel-selector/DefaultDesktopAgentChannelSelector.ts delete mode 100644 fdc3-for-web-implementation/packages/client/src/intent-resolution/DefaultDesktopAgentIntentResolver.ts delete mode 100644 fdc3-for-web-implementation/packages/client/src/intent-resolution/NullIIntentResolver.ts create mode 100644 fdc3-for-web-implementation/packages/client/src/ui/AbstractUIComponent.ts create mode 100644 fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentChannelSelector.ts create mode 100644 fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts rename fdc3-for-web-implementation/packages/client/src/{channel-selector => ui}/NullChannelSelector.ts (78%) create mode 100644 fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts create mode 100644 fdc3-for-web-implementation/packages/da-proxy/test/features/raise-intents.feature create mode 100644 fdc3-for-web-implementation/packages/da-proxy/test/support/responses/RaiseIntentForContext.ts create mode 100644 fdc3-for-web-implementation/packages/demo/src/client/apps/app7.ts create mode 100644 fdc3-for-web-implementation/packages/demo/static/app7/index.html create mode 100644 fdc3-for-web-implementation/packages/fdc3-common/src/ChannelSelector.ts create mode 100644 fdc3-for-web-implementation/packages/fdc3-common/src/IntentResolver.ts diff --git a/fdc3-for-web-implementation/packages/client/package.json b/fdc3-for-web-implementation/packages/client/package.json index a694b054b..8fd084faa 100644 --- a/fdc3-for-web-implementation/packages/client/package.json +++ b/fdc3-for-web-implementation/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/client", - "version": "0.0.50", + "version": "0.0.54", "files": [ "dist" ], @@ -13,14 +13,14 @@ }, "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", - "@kite9/da-proxy": "0.0.50", - "@kite9/fdc3-common": "0.0.50", + "@kite9/da-proxy": "0.0.54", + "@kite9/fdc3-common": "0.0.54", "@types/uuid": "^10.0.0", "uuid": "^9.0.1" }, "devDependencies": { "@cucumber/cucumber": "10.3.1", - "@kite9/da-server": "0.0.50", + "@kite9/da-server": "0.0.54", "@types/node": "^20.14.11", "expect": "^29.7.0", "jsonpath-plus": "^9.0.0", diff --git a/fdc3-for-web-implementation/packages/client/src/channel-selector/DefaultDesktopAgentChannelSelector.ts b/fdc3-for-web-implementation/packages/client/src/channel-selector/DefaultDesktopAgentChannelSelector.ts deleted file mode 100644 index 58a6c342f..000000000 --- a/fdc3-for-web-implementation/packages/client/src/channel-selector/DefaultDesktopAgentChannelSelector.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { ChannelSelector, ChannelSelectorDetails, CSS_ELEMENTS, CSSPositioning, SelectorMessageChannels, SelectorMessageChoice, SelectorMessageResize } from "@kite9/fdc3-common"; -import { Channel } from "@finos/fdc3"; - - - - -const DEFAULT_CHANNEL_SELECTOR_DETAILS: ChannelSelectorDetails = { - uri: "http://localhost:4000/channel_selector.html", - collapsedCss: { - position: "fixed", - zIndex: "1000", - right: "10px", - bottom: "10px", - width: "50px", - height: "50px" - }, - expandedCss: { - position: "fixed", - zIndex: "1000", - right: "10px", - bottom: "10px", - width: "450px", - maxHeight: "600px", - transition: "all 0.5s ease-out allow-discrete" - } -} - -/** - * Works with the desktop agent to provide a simple channel selector. - * - * This is the default implementation, but can be overridden by app implementers calling - * the getAgentApi() method - */ -export class DefaultDesktopAgentChannelSelector implements ChannelSelector { - - private readonly details: ChannelSelectorDetails - private container: HTMLDivElement | undefined = undefined - private iframe: Window | undefined = undefined - private availableChannels: Channel[] = [] - private channelId: string | null = null - private callback: ((channelId: string) => void) | null = null - private port: MessagePort | undefined = undefined - - constructor(url: string | null) { - this.details = { - ...DEFAULT_CHANNEL_SELECTOR_DETAILS, - uri: url ?? DEFAULT_CHANNEL_SELECTOR_DETAILS.uri!! - } - - this.setupMessageListener() - this.openFrame() - } - - themeContainer(css: CSSPositioning) { - for (let i = 0; i < CSS_ELEMENTS.length; i++) { - const k = CSS_ELEMENTS[i] - const value: string | undefined = css[(k as string)] - if (value != null) { - this.container?.style.setProperty(k, value) - } else { - this.container?.style.removeProperty(k) - } - } - } - - themeFrame(ifrm: HTMLIFrameElement) { - ifrm.setAttribute("name", "FDC3 Channel Selector") - ifrm.style.width = "100%" - ifrm.style.height = "100%" - ifrm.style.border = "0" - } - - private setupMessageListener() { - globalThis.window.addEventListener("message", (e) => { - if (e.source == this.iframe && e.data.type == 'SelectorMessageInitialize') { - this.port = e.ports[0] - this.port.start() - this.port.onmessage = (e) => { - switch (e.data.type) { - case 'SelectorMessageChoice': - const choice = e.data as SelectorMessageChoice - if ((choice.channelId) && (this.callback)) { - this.callback(choice.channelId) - } - break - case 'SelectorMessageResize': - const resize = e.data as SelectorMessageResize - if (resize.expanded) { - this.themeContainer(this.details.expandedCss!!) - } else { - this.themeContainer(this.details.collapsedCss!!) - } - break - } - } - - // send the available channels details - this.updateChannel(this.channelId, this.availableChannels) - } - }) - } - - serializeChannels(): any { - return this.availableChannels.map(ch => { - return { - id: ch.id, - displayMetadata: ch.displayMetadata - } - }) - } - - updateChannel(channelId: string | null, availableChannels: Channel[]): void { - // record the settings here - this.channelId = channelId - this.availableChannels = availableChannels - - // also send to the iframe - this.port?.postMessage({ - type: 'SelectorMessageChannels', - channels: this.availableChannels.map(ch => { - return { - id: ch.id, - displayMetadata: ch.displayMetadata - } - }), - selected: channelId - } as SelectorMessageChannels) - } - - private openFrame(): void { - this.container = globalThis.document.createElement("div") - const ifrm = globalThis.document.createElement("iframe") - - this.themeContainer(this.details.collapsedCss ?? DEFAULT_CHANNEL_SELECTOR_DETAILS.collapsedCss!!) - this.themeFrame(ifrm) - - ifrm.setAttribute("src", this.details.uri!!) - this.container.appendChild(ifrm) - document.body.appendChild(this.container) - this.iframe = ifrm.contentWindow!! - } - - setChannelChangeCallback(callback: (channelId: string) => void): void { - this.callback = callback - } - -} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/intent-resolution/DefaultDesktopAgentIntentResolver.ts b/fdc3-for-web-implementation/packages/client/src/intent-resolution/DefaultDesktopAgentIntentResolver.ts deleted file mode 100644 index f41854d2c..000000000 --- a/fdc3-for-web-implementation/packages/client/src/intent-resolution/DefaultDesktopAgentIntentResolver.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { AppIdentifier, AppIntent, IntentResult } from "@finos/fdc3"; -import { IntentResolver, SingleAppIntent, IntentResolverDetails, CSS_ELEMENTS, ResolverMessageChoice, ResolverIntents } from "@kite9/fdc3-common"; - -export const DEFAULT_INTENT_RESOLVER_DETAILS: IntentResolverDetails = { - uri: "http://localhost:4000/intent_resolver.html", - css: { - position: "fixed", - zIndex: "1000", - left: "10%", - top: "10%", - right: "10%", - bottom: "10%" - } -} - -/** - * Works with the desktop agent to provide a resolution to the intent choices. - * This is the default implementation, but can be overridden by app implementers calling - * the getAgentApi() method - */ -export class DefaultDesktopAgentIntentResolver implements IntentResolver { - - private readonly details: IntentResolverDetails - private container: HTMLDivElement | undefined = undefined - - constructor(url: string | null) { - this.details = { - ...DEFAULT_INTENT_RESOLVER_DETAILS, - uri: url ?? DEFAULT_INTENT_RESOLVER_DETAILS.uri!! - } - } - - async intentChosen(ir: IntentResult): Promise { - this.removeFrame() - return ir - } - - async chooseIntent(appIntents: AppIntent[], source: AppIdentifier) { - const iframe = await this.openFrame() - const chosen = await this.receiveChosenIntent(iframe, appIntents, source) - this.removeFrame() - return chosen - } - - async receiveChosenIntent(iframe: Window, appIntents: AppIntent[], source: AppIdentifier): Promise { - return new Promise((resolve, _reject) => { - window.addEventListener("message", (e) => { - if (e.source == iframe && e.data.type == 'SelectorMessageInitialize') { - const port = e.ports[0] - port.start() - port.onmessage = (e) => { - switch (e.data.type) { - case 'ResolverMessageChoice': - const choice = e.data as ResolverMessageChoice - resolve(choice.payload) - - } - } - - // send the available channels details - port.postMessage({ - type: "ResolverIntents", - appIntents, - source - } as ResolverIntents) - } - }) - }) - } - - removeFrame() { - if (this.container) { - document.body.removeChild(this.container) - this.container = undefined - } - } - - themeContainer(container: HTMLDivElement) { - const css = this.details.css ?? DEFAULT_INTENT_RESOLVER_DETAILS.css!! - for (let i = 0; i < CSS_ELEMENTS.length; i++) { - const k = CSS_ELEMENTS[i] - const value: string | undefined = css[(k as string)] - if (value != null) { - container.style.setProperty(k, value) - } else { - this.container?.style.removeProperty(k) - } - - } - } - - themeFrame(ifrm: HTMLIFrameElement) { - ifrm.setAttribute("name", "FDC3 Intent Resolver") - ifrm.style.width = "100%" - ifrm.style.height = "100%" - ifrm.style.border = "0" - } - - async openFrame(): Promise { - this.removeFrame() - - this.container = document.createElement("div") - const ifrm = document.createElement("iframe") - - this.themeContainer(this.container) - this.themeFrame(ifrm) - - ifrm.setAttribute("src", this.details.uri ?? DEFAULT_INTENT_RESOLVER_DETAILS.uri!!) - - this.container.appendChild(ifrm) - document.body.appendChild(this.container) - return ifrm.contentWindow!! - } -} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/intent-resolution/NullIIntentResolver.ts b/fdc3-for-web-implementation/packages/client/src/intent-resolution/NullIIntentResolver.ts deleted file mode 100644 index f3572314d..000000000 --- a/fdc3-for-web-implementation/packages/client/src/intent-resolution/NullIIntentResolver.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { AppIntent, AppIdentifier, IntentResult } from "@finos/fdc3"; -import { IntentResolver, SingleAppIntent } from "@kite9/fdc3-common"; - - -export class NullIntentResolver implements IntentResolver { - - chooseIntent(_appIntents: AppIntent[], _source: AppIdentifier): Promise { - throw new Error("Method not implemented."); - } - - intentChosen(_intentResult: IntentResult): Promise { - throw new Error("Method not implemented."); - } - -} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts b/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts index 194939e48..cbc220a73 100644 --- a/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts +++ b/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts @@ -1,10 +1,11 @@ import { DesktopAgent } from "@finos/fdc3"; -import { BasicDesktopAgent, DefaultChannelSupport, DefaultAppSupport, DefaultIntentSupport, DefaultHandshakeSupport } from "@kite9/da-proxy"; +import { BasicDesktopAgent, DefaultChannelSupport, DefaultAppSupport, DefaultIntentSupport, DefaultHandshakeSupport, ChannelSupport } from "@kite9/da-proxy"; import { ConnectionDetails, MessagePortMessaging } from "./MessagePortMessaging"; -import { DefaultDesktopAgentIntentResolver } from "../intent-resolution/DefaultDesktopAgentIntentResolver"; -import { DefaultDesktopAgentChannelSelector } from "../channel-selector/DefaultDesktopAgentChannelSelector"; -import { NullIntentResolver } from "../intent-resolution/NullIIntentResolver"; -import { NullChannelSelector } from "../channel-selector/NullChannelSelector"; +import { DefaultDesktopAgentIntentResolver } from "../ui/DefaultDesktopAgentIntentResolver"; +import { DefaultDesktopAgentChannelSelector } from "../ui/DefaultDesktopAgentChannelSelector"; +import { NullIntentResolver } from "../ui/NullIIntentResolver"; +import { NullChannelSelector } from "../ui/NullChannelSelector"; +import { ChannelSelector } from "@kite9/fdc3-common"; /** * Given a message port, constructs a desktop agent to communicate via that. @@ -34,14 +35,25 @@ export async function createDesktopAgentAPI(cd: ConnectionDetails): Promise { + const channel = await cs.getUserChannel() + const userChannels = await cs.getUserChannels() + channelSelector.updateChannel(channel?.id ?? null, userChannels) +} diff --git a/fdc3-for-web-implementation/packages/client/src/ui/AbstractUIComponent.ts b/fdc3-for-web-implementation/packages/client/src/ui/AbstractUIComponent.ts new file mode 100644 index 000000000..c42925b41 --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/src/ui/AbstractUIComponent.ts @@ -0,0 +1,115 @@ +export interface CSSPositioning { [key: string]: string } + +export const INITIAL_CONTAINER_CSS = { + width: "0", + height: "0", + right: "20px", + bottom: "20px", + position: "fixed" +} + + +export const ALLOWED_CSS_ELEMENTS = [ + "width", + "height", + "position", + "zIndex", + "left", + "right", + "top", + "bottom", + "transition", + "maxHeight", + "maxWidth", + "display" +] + +export abstract class AbstractUIComponent { + + private container: HTMLDivElement | undefined = undefined + private iframe: Window | undefined = undefined + private url: string + private name: string + + constructor(url: string, name: string) { + this.url = url + this.name = name + } + + async init() { + const portPromise = this.awaitHello() + this.openFrame() + const port = await portPromise + await this.setupMessagePort(port) + await this.messagePortReady(port) + } + + /** + * Override and extend this method to provide functionality specific to the UI in question + */ + async setupMessagePort(port: MessagePort): Promise { + port.addEventListener("message", (e) => { + const data = e.data + if (data.type == 'iframeRestyle') { + console.log(`Restyling ${JSON.stringify(data.payload)}`) + const css = data.payload.css + this.themeContainer(css) + } + }) + } + + async messagePortReady(port: MessagePort) { + // tells the iframe it can start posting + port.postMessage({ type: "iframeHandshake" }) + } + + private awaitHello(): Promise { + return new Promise((resolve, _reject) => { + const ml = (e: MessageEvent) => { + console.log("Received UI Message: " + JSON.stringify(e.data)) + if ((e.source == this.iframe) && (e.data.type == 'iframeHello')) { + const port = e.ports[0] + port.start() + globalThis.window.removeEventListener("message", ml) + resolve(port) + } + } + + globalThis.window.addEventListener("message", ml) + }); + + } + + private openFrame(): void { + this.container = globalThis.document.createElement("div") + const ifrm = globalThis.document.createElement("iframe") + + this.themeContainer(INITIAL_CONTAINER_CSS) + this.themeFrame(ifrm) + + ifrm.setAttribute("src", this.url) + this.container.appendChild(ifrm) + document.body.appendChild(this.container) + this.iframe = ifrm.contentWindow!! + } + + themeContainer(css: CSSPositioning) { + for (let i = 0; i < ALLOWED_CSS_ELEMENTS.length; i++) { + const k = ALLOWED_CSS_ELEMENTS[i] + const value: string | undefined = css[(k as string)] + if (value != null) { + this.container?.style.setProperty(k, value) + } else { + this.container?.style.removeProperty(k) + } + } + } + + themeFrame(ifrm: HTMLIFrameElement) { + ifrm.setAttribute("name", this.name) + ifrm.style.width = "100%" + ifrm.style.height = "100%" + ifrm.style.border = "0" + } + +} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentChannelSelector.ts b/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentChannelSelector.ts new file mode 100644 index 000000000..70d55a78c --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentChannelSelector.ts @@ -0,0 +1,56 @@ +import { IframeChannels, IframeChannelSelected, ChannelSelector } from "@kite9/fdc3-common"; +import { Channel } from "@finos/fdc3"; +import { AbstractUIComponent } from "./AbstractUIComponent"; + + +/** + * Works with the desktop agent to provide a simple channel selector. + * + * This is the default implementation, but can be overridden by app implementers calling + * the getAgentApi() method + */ +export class DefaultDesktopAgentChannelSelector extends AbstractUIComponent implements ChannelSelector { + + private callback: ((channelId: string) => void) | null = null + private port: MessagePort | null = null + + constructor(url: string | null) { + super(url ?? "https://fdc3.finos.org/webui/channel_selector.html", "FDC3 Channel Selector") + } + + async setupMessagePort(port: MessagePort): Promise { + await super.setupMessagePort(port) + this.port = port + + port.addEventListener("message", (e) => { + if (e.data.type == 'iframeChannelSelected') { + const choice = e.data as IframeChannelSelected + if ((choice.payload.selected) && (this.callback)) { + this.callback(choice.payload.selected) + } + } + }) + } + + updateChannel(channelId: string | null, availableChannels: Channel[]): void { + // also send to the iframe + this.port!!.postMessage({ + type: 'iframeChannels', + payload: { + selected: channelId, + userChannels: availableChannels.map(ch => { + return { + id: ch.id, + displayMetadata: ch.displayMetadata + } + }) + } + + } as IframeChannels) + } + + setChannelChangeCallback(callback: (channelId: string) => void): void { + this.callback = callback + } + +} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts b/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts new file mode 100644 index 000000000..9333c0c7c --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts @@ -0,0 +1,56 @@ +import { AppIdentifier, AppIntent } from "@finos/fdc3"; +import { IframeResolveAction, Context, IframeResolve, IntentResolver, IntentResolutionChoice } from "@kite9/fdc3-common"; +import { AbstractUIComponent } from "./AbstractUIComponent"; + +/** + * Works with the desktop agent to provide a resolution to the intent choices. + * This is the default implementation, but can be overridden by app implementers calling + * the getAgentApi() method + */ +export class DefaultDesktopAgentIntentResolver extends AbstractUIComponent implements IntentResolver { + + private port: MessagePort | null = null + private pendingResolve: ((x: IntentResolutionChoice | void) => void) | null = null + + constructor(url: string | null) { + super(url ?? "https://fdc3.finos.org/webui/channel_selector.html", "FDC3 Intent Resolver") + } + + async setupMessagePort(port: MessagePort): Promise { + await super.setupMessagePort(port) + this.port = port + + this.port.addEventListener("message", (e) => { + if (e.data.type == 'iframeResolveAction') { + const choice = e.data as IframeResolveAction + if ((choice.payload.action == 'click') && (this.pendingResolve)) { + this.pendingResolve({ + appId: choice.payload.appIdentifier!!, + intent: choice.payload.intent!! + }) + } else if ((choice.payload.action == 'cancel') && (this.pendingResolve)) { + this.pendingResolve() + } + + this.pendingResolve = null + } + }) + } + + async chooseIntent(appIntents: AppIntent[], context: Context): Promise { + const out = new Promise((resolve, _reject) => { + this.pendingResolve = resolve + }) + + + this.port?.postMessage({ + type: 'iframeResolve', + payload: { + appIntents, + context + } + } as IframeResolve) + + return out + } +} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/channel-selector/NullChannelSelector.ts b/fdc3-for-web-implementation/packages/client/src/ui/NullChannelSelector.ts similarity index 78% rename from fdc3-for-web-implementation/packages/client/src/channel-selector/NullChannelSelector.ts rename to fdc3-for-web-implementation/packages/client/src/ui/NullChannelSelector.ts index 05c52b229..e1a4a2ab6 100644 --- a/fdc3-for-web-implementation/packages/client/src/channel-selector/NullChannelSelector.ts +++ b/fdc3-for-web-implementation/packages/client/src/ui/NullChannelSelector.ts @@ -1,8 +1,10 @@ -import { Channel } from "@finos/fdc3"; +import { Channel, } from "@finos/fdc3"; import { ChannelSelector } from "@kite9/fdc3-common"; export class NullChannelSelector implements ChannelSelector { + async init(): Promise { + } updateChannel(_channelId: string | null, _availableChannels: Channel[]): void { } diff --git a/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts b/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts new file mode 100644 index 000000000..dd68a0381 --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts @@ -0,0 +1,14 @@ +import { AppIntent } from "@finos/fdc3"; +import { Context, IntentResolver } from "@kite9/fdc3-common"; +import { IntentResolutionChoice } from "@kite9/fdc3-common/src/IntentResolver"; + + +export class NullIntentResolver implements IntentResolver { + + async init(): Promise { + } + + chooseIntent(_appIntents: AppIntent[], _ctx: Context): Promise { + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/da-proxy/package.json b/fdc3-for-web-implementation/packages/da-proxy/package.json index 57e387583..3c74f8446 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/package.json +++ b/fdc3-for-web-implementation/packages/da-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/da-proxy", - "version": "0.0.50", + "version": "0.0.54", "files": [ "dist" ], @@ -13,7 +13,7 @@ }, "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", - "@kite9/fdc3-common": "0.0.50" + "@kite9/fdc3-common": "0.0.54" }, "devDependencies": { "@cucumber/cucumber": "10.3.1", @@ -34,7 +34,7 @@ "is-ci": "2.0.0", "jsonpath-plus": "^9.0.0", "nyc": "15.1.0", - "@kite9/testing": "0.0.50", + "@kite9/testing": "0.0.54", "prettier": "2.2.1", "rimraf": "^6.0.1", "ts-node": "^10.9.2", diff --git a/fdc3-for-web-implementation/packages/da-proxy/src/channels/DefaultChannelSupport.ts b/fdc3-for-web-implementation/packages/da-proxy/src/channels/DefaultChannelSupport.ts index ca676add3..7f535fcc8 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/src/channels/DefaultChannelSupport.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/src/channels/DefaultChannelSupport.ts @@ -24,6 +24,10 @@ const NO_OP_CHANNEL_SELECTOR: ChannelSelector = { setChannelChangeCallback(_callback: (channelId: string) => void): void { // also does nothing + }, + + init: function (): Promise { + return Promise.resolve() } } @@ -107,6 +111,7 @@ export class DefaultChannelSupport implements ChannelSupport { } as LeaveCurrentChannelRequest, 'leaveCurrentChannelResponse') + this.channelSelector.updateChannel(null, this.userChannels ?? []) this.followingListeners.forEach(l => l.changeChannel(null)) } @@ -120,6 +125,8 @@ export class DefaultChannelSupport implements ChannelSupport { } as JoinUserChannelRequest, 'joinUserChannelResponse') + this.channelSelector.updateChannel(id, this.userChannels ?? []) + for (const l of this.followingListeners) { await l.changeChannel(new DefaultChannel(this.messaging, id, "user")) } diff --git a/fdc3-for-web-implementation/packages/da-proxy/src/intents/DefaultIntentSupport.ts b/fdc3-for-web-implementation/packages/da-proxy/src/intents/DefaultIntentSupport.ts index edf567956..151ac4528 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/src/intents/DefaultIntentSupport.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/src/intents/DefaultIntentSupport.ts @@ -3,7 +3,7 @@ import { IntentSupport } from "./IntentSupport"; import { Messaging } from "../Messaging"; import { DefaultIntentResolution } from "./DefaultIntentResolution"; import { DefaultIntentListener } from "../listeners/DefaultIntentListener"; -import { IntentResolver, RaiseIntentForContextRequest, RaiseIntentForContextResponse } from "@kite9/fdc3-common"; +import { IntentResolutionChoice, IntentResolver, RaiseIntentForContextRequest, RaiseIntentForContextResponse } from "@kite9/fdc3-common"; import { DefaultChannel } from "../channels/DefaultChannel"; import { DefaultPrivateChannel } from "../channels/DefaultPrivateChannel"; import { FindIntentRequest, FindIntentResponse, AddContextListenerRequestMeta, FindIntentsByContextRequest, FindIntentsByContextResponse, RaiseIntentRequest, RaiseIntentResultResponse, RaiseIntentResponse } from "@kite9/fdc3-common" @@ -91,12 +91,12 @@ export class DefaultIntentSupport implements IntentSupport { return; } else { const ir = await convertIntentResult(rp, this.messaging) - this.intentResolver.intentChosen(ir) return ir } } async raiseIntent(intent: string, context: Context, app: AppIdentifier): Promise { + const meta = this.messaging.createMeta() const messageOut: RaiseIntentRequest = { type: "raiseIntentRequest", payload: { @@ -104,19 +104,34 @@ export class DefaultIntentSupport implements IntentSupport { context, app: app }, - meta: this.messaging.createMeta() as AddContextListenerRequestMeta /* ISSUE: #1275 */ + meta: meta as any /* ISSUE: #1275 */ } const resultPromise = this.createResultPromise(messageOut) - const resolution = await this.messaging.exchange(messageOut, "raiseIntentResponse", ResolveError.IntentDeliveryFailed) as RaiseIntentResponse - const details = resolution.payload.intentResolution!! - - return new DefaultIntentResolution( - this.messaging, - resultPromise, - details.source, - details.intent - ) + const response = await this.messaging.exchange(messageOut, "raiseIntentResponse", ResolveError.IntentDeliveryFailed) as RaiseIntentResponse + + if (response.payload.appIntent) { + // we need to invoke the resolver + const result: IntentResolutionChoice | void = await this.intentResolver.chooseIntent([response.payload.appIntent as any], context) + if (result) { + return new DefaultIntentResolution( + this.messaging, + resultPromise, + result.appId, + result.intent) + } else { + throw new Error(ResolveError.UserCancelled) + } + } else { + // single intent + const details = response.payload.intentResolution!! + return new DefaultIntentResolution( + this.messaging, + resultPromise, + details.source, + details.intent + ) + } } async raiseIntentForContext(context: Context, app?: AppIdentifier | undefined): Promise { @@ -130,15 +145,28 @@ export class DefaultIntentSupport implements IntentSupport { } const resultPromise = this.createResultPromise(messageOut) - const resolution = await this.messaging.exchange(messageOut, "raiseIntentForContextResponse", ResolveError.IntentDeliveryFailed) as RaiseIntentForContextResponse - const details = resolution.payload.intentResolution!! - - return new DefaultIntentResolution( - this.messaging, - resultPromise, - details.source, - details.intent - ) + const response = await this.messaging.exchange(messageOut, "raiseIntentForContextResponse", ResolveError.IntentDeliveryFailed) as RaiseIntentForContextResponse + if (response.payload.appIntents) { + // we need to invoke the resolver + const result: IntentResolutionChoice | void = await this.intentResolver.chooseIntent(response.payload.appIntents as any[], context) + if (result) { + return new DefaultIntentResolution( + this.messaging, + resultPromise, + result.appId, + result.intent) + } else { + throw new Error(ResolveError.UserCancelled) + } + } else { + const details = response.payload.intentResolution!! + return new DefaultIntentResolution( + this.messaging, + resultPromise, + details.source, + details.intent + ) + } } async addIntentListener(intent: string, handler: IntentHandler): Promise { diff --git a/fdc3-for-web-implementation/packages/da-proxy/test/features/raise-intents.feature b/fdc3-for-web-implementation/packages/da-proxy/test/features/raise-intents.feature new file mode 100644 index 000000000..f3a72799a --- /dev/null +++ b/fdc3-for-web-implementation/packages/da-proxy/test/features/raise-intents.feature @@ -0,0 +1,71 @@ +Feature: Basic Intents Support + + Background: Desktop Agent API + Given A Desktop Agent in "api" + And schemas loaded + And app "chipShop/c1" resolves intent "OrderFood" with result type "void" + And app "chipShop/c2" resolves intent "OrderFood" with result type "channel" + And app "bank/b1" resolves intent "Buy" with context "fdc3.instrument" and result type "fdc3.order" + And app "bank/b1" resolves intent "Sell" with context "fdc3.instrument" and result type "fdc3.order" + And app "travelAgent/t1" resolves intent "BookFlight" with context "fdc3.country" and result type "fdc3.order" + And app "notused/n1" resolves intent "Buy" with context "fdc3.cancel-me" and result type "fdc3.order" + And app "notused/n2" resolves intent "Buy" with context "fdc3.cancel-me" and result type "fdc3.order" + And "instrumentContext" is a "fdc3.instrument" context + And "countryContext" is a "fdc3.country" context + And "cancelContext" is a "fdc3.cancel-me" context + + Scenario: Raising an intent and invoking the intent resolver when it's not clear which intent is required + The intent resolver will just take the first matching application + that would resolve the intent. + + When I call "{api}" with "raiseIntent" with parameters "OrderFood" and "{instrumentContext}" + Then "{result}" is an object with the following contents + | source.appId | source.instanceId | + | chipShop | c1 | + And messaging will have posts + | payload.intent | payload.context.type | payload.context.id.ticker | matches_type | + | OrderFood | fdc3.instrument | AAPL | raiseIntentRequest | + + Scenario: Raising an intent and invoking the intent resolver, but the user cancels it. + When I call "{api}" with "raiseIntent" with parameters "OrderFood" and "{cancelContext}" + Then "{result}" is an error with message "UserCancelledResolution" + And messaging will have posts + | payload.intent | payload.context.type | matches_type | + | OrderFood | fdc3.cancel-me | raiseIntentRequest | + + Scenario: Raising Intent exactly right, so the resolver isn't required + When I call "{api}" with "raiseIntent" with parameters "Buy" and "{instrumentContext}" + Then "{result}" is an object with the following contents + | source.appId | source.instanceId | + | bank | b1 | + And messaging will have posts + | payload.intent | payload.context.type | payload.context.id.ticker | matches_type | + | Buy | fdc3.instrument | AAPL | raiseIntentRequest | + + Scenario: Raising Intent By Context and invoking the intent resolver when it's not clear which intent is required + The intent resolver will just take the first matching application + that would resolve an intent. + + When I call "{api}" with "raiseIntentForContext" with parameter "{instrumentContext}" + Then "{result}" is an object with the following contents + | source.appId | source.instanceId | + | chipShop | c1 | + And messaging will have posts + | payload.context.type | payload.context.id.ticker | matches_type | + | fdc3.instrument | AAPL | raiseIntentForContextRequest | + + Scenario: Raising Intent By Context exactly right, so the resolver isn't required + When I call "{api}" with "raiseIntentForContext" with parameters "{countryContext}" and "{t1}" + Then "{result}" is an object with the following contents + | source.appId | source.instanceId | + | travelAgent | t1 | + And messaging will have posts + | payload.context.type | payload.context.name | payload.app.appId | payload.app.instanceId | matches_type | + | fdc3.country | Sweden | travelAgent | t1 | raiseIntentForContextRequest | + + Scenario: Raising an intent and invoking the intent resolver, but the user cancels it. + When I call "{api}" with "raiseIntentForContext" with parameter "{cancelContext}" + Then "{result}" is an error with message "UserCancelledResolution" + And messaging will have posts + | payload.context.type | matches_type | + | fdc3.cancel-me | raiseIntentForContextRequest | diff --git a/fdc3-for-web-implementation/packages/da-proxy/test/step-definitions/channels.steps.ts b/fdc3-for-web-implementation/packages/da-proxy/test/step-definitions/channels.steps.ts index 6f753b618..c17eaed77 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/test/step-definitions/channels.steps.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/test/step-definitions/channels.steps.ts @@ -24,6 +24,9 @@ const contextMap: Record = { "fdc3.unsupported": { "type": "fdc3.unsupported", "bogus": true + }, + "fdc3.cancel-me": { + "type": "fdc3.cancel-me" } } diff --git a/fdc3-for-web-implementation/packages/da-proxy/test/support/TestMessaging.ts b/fdc3-for-web-implementation/packages/da-proxy/test/support/TestMessaging.ts index d353ec1fa..2def15405 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/test/support/TestMessaging.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/test/support/TestMessaging.ts @@ -19,6 +19,7 @@ import { UnsubscribeListeners } from "./responses/UnsubscribeListeners"; import { CreatePrivateChannel } from "./responses/CreatePrivateChannel"; import { DisconnectPrivateChannel } from "./responses/DisconnectPrivateChannel"; import { IntentResult } from "./responses/IntentResult"; +import { RaiseIntentForContext } from "./responses/RaiseIntentForContext"; export interface IntentDetail { app?: AppIdentifier, @@ -112,6 +113,7 @@ export class TestMessaging extends AbstractMessaging { new FindIntent(), new FindIntentByContext(), new RaiseIntent(), + new RaiseIntentForContext(), new IntentResult(), new GetAppMetadata(), new FindInstances(), @@ -200,8 +202,7 @@ export class TestMessaging extends AbstractMessaging { }) } - private ir: PossibleIntentResult = { - } + private ir: PossibleIntentResult | null = null getIntentResult() { return this.ir diff --git a/fdc3-for-web-implementation/packages/da-proxy/test/support/responses/RaiseIntent.ts b/fdc3-for-web-implementation/packages/da-proxy/test/support/responses/RaiseIntent.ts index b98611107..4951f5b48 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/test/support/responses/RaiseIntent.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/test/support/responses/RaiseIntent.ts @@ -1,5 +1,6 @@ -import { AutomaticResponse, IntentDetail, TestMessaging } from "../TestMessaging"; +import { AutomaticResponse, IntentDetail, intentDetailMatches, TestMessaging } from "../TestMessaging"; import { RaiseIntentRequest, RaiseIntentResponse, RaiseIntentResultResponse } from "@kite9/fdc3-common"; +import { ResolveError } from "@finos/fdc3" export class RaiseIntent implements AutomaticResponse { @@ -7,8 +8,8 @@ export class RaiseIntent implements AutomaticResponse { return t == 'raiseIntentRequest' } - createRaiseIntentAgentResponseMessage(intentRequest: RaiseIntentRequest, using: IntentDetail, m: TestMessaging): RaiseIntentResponse { - const result = m.getIntentResult() + createCannedRaiseIntentResponseMessage(intentRequest: RaiseIntentRequest, m: TestMessaging): RaiseIntentResponse { + const result = m.getIntentResult()!! if (result.error) { const out: RaiseIntentResponse = { meta: { @@ -30,8 +31,11 @@ export class RaiseIntent implements AutomaticResponse { }, payload: { intentResolution: { - intent: using.intent!!, - source: using.app!! + intent: intentRequest.payload.intent, + source: { + appId: "some-app", + instanceId: "abc123" + } } }, type: "raiseIntentResponse" @@ -41,8 +45,59 @@ export class RaiseIntent implements AutomaticResponse { } } + private createRaiseIntentResponseMessage(intentRequest: RaiseIntentRequest, relevant: IntentDetail[], m: TestMessaging): RaiseIntentResponse { + if (relevant.length == 0) { + return { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + type: "raiseIntentResponse", + payload: { + error: ResolveError.NoAppsFound + } + } + } else if (relevant.length == 1) { + return { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + type: "raiseIntentResponse", + payload: { + intentResolution: { + intent: relevant[0].intent!!, + source: relevant[0].app!! + } + } + } + } else { + return { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + type: "raiseIntentResponse", + payload: { + appIntent: { + apps: relevant.map(r => { + return { + appId: r?.app?.appId!!, + instanceId: r?.app?.instanceId + } + }), + intent: { + displayName: intentRequest.payload.intent, + name: intentRequest.payload.intent + } + } + } + } + } + } + createRaiseIntentResultResponseMesssage(intentRequest: RaiseIntentRequest, m: TestMessaging): RaiseIntentResultResponse | undefined { - const result = m.getIntentResult() + const result = m.getIntentResult()!! if (result.error) { return undefined } else { @@ -52,7 +107,7 @@ export class RaiseIntent implements AutomaticResponse { responseUuid: m.createUUID() }, payload: { - intentResult: m.getIntentResult() + intentResult: m.getIntentResult()!! }, type: "raiseIntentResultResponse" } @@ -67,16 +122,23 @@ export class RaiseIntent implements AutomaticResponse { const payload = intentRequest.payload const intent = payload.intent const context = payload?.context?.type - const app = payload?.app - const using: IntentDetail = { - intent, - context, - app - } - if (!m.getIntentResult()?.timeout) { - // this sends out the intent resolution - const out1 = this.createRaiseIntentAgentResponseMessage(intentRequest, using, m) + + if (m.getIntentResult() == undefined) { + // we're going to figure out the right response based on the app details (a la FindIntent) + const app = payload?.app + const using: IntentDetail = { + intent, + context, + app + } + + const relevant = m.intentDetails.filter(id => intentDetailMatches(id, using, false)) + const request = this.createRaiseIntentResponseMessage(intentRequest, relevant, m) + setTimeout(() => { m.receive(request) }, 100) + } else if (!m.getIntentResult()?.timeout) { + // this sends out the pre-set intent resolution + const out1 = this.createCannedRaiseIntentResponseMessage(intentRequest, m) setTimeout(() => { m.receive(out1) }, 100) // next, send the result response @@ -85,6 +147,7 @@ export class RaiseIntent implements AutomaticResponse { setTimeout(() => { m.receive(out2) }, 300) } } + return Promise.resolve() } } diff --git a/fdc3-for-web-implementation/packages/da-proxy/test/support/responses/RaiseIntentForContext.ts b/fdc3-for-web-implementation/packages/da-proxy/test/support/responses/RaiseIntentForContext.ts new file mode 100644 index 000000000..cb160a3ce --- /dev/null +++ b/fdc3-for-web-implementation/packages/da-proxy/test/support/responses/RaiseIntentForContext.ts @@ -0,0 +1,147 @@ +import { AutomaticResponse, IntentDetail, intentDetailMatches, TestMessaging } from "../TestMessaging"; +import { RaiseIntentForContextRequest, RaiseIntentForContextResponse, RaiseIntentResultResponse } from "@kite9/fdc3-common"; +import { ResolveError } from "@finos/fdc3" + +export class RaiseIntentForContext implements AutomaticResponse { + + filter(t: string) { + return t == 'raiseIntentForContextRequest' + } + + createCannedRaiseIntentForContextResponseMessage(intentRequest: RaiseIntentForContextRequest, m: TestMessaging): RaiseIntentForContextResponse { + const result = m.getIntentResult()!! + if (result.error) { + const out: RaiseIntentForContextResponse = { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + payload: { + error: result.error as any + }, + type: "raiseIntentForContextResponse" + } + + return out + } else { + const out: RaiseIntentForContextResponse = { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + payload: { + intentResolution: { + intent: "some-canned-intent", + source: { + appId: "some-app", + instanceId: "abc123" + } + } + }, + type: "raiseIntentForContextResponse" + } + + return out + } + } + + private createRaiseIntentForContextResponseMessage(intentRequest: RaiseIntentForContextRequest, relevant: IntentDetail[], m: TestMessaging): RaiseIntentForContextResponse { + if (relevant.length == 0) { + return { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + type: "raiseIntentForContextResponse", + payload: { + error: ResolveError.NoAppsFound + } + } + } else if (relevant.length == 1) { + return { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + type: "raiseIntentForContextResponse", + payload: { + intentResolution: { + intent: relevant[0].intent!!, + source: relevant[0].app!! + } + } + } + } else { + return { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + type: "raiseIntentForContextResponse", + payload: { + appIntents: relevant.map(r => { + return { + apps: [r.app!!], + intent: { + name: r.intent!! + } + } + }) + } + } + } + } + + createRaiseIntentResultResponseMesssage(intentRequest: RaiseIntentForContextRequest, m: TestMessaging): RaiseIntentResultResponse | undefined { + const result = m.getIntentResult()!! + if (result.error) { + return undefined + } else { + const out: RaiseIntentResultResponse = { + meta: { + ...intentRequest.meta, + responseUuid: m.createUUID() + }, + payload: { + intentResult: m.getIntentResult()!! + }, + type: "raiseIntentResultResponse" + } + + return out + } + + } + + + action(input: object, m: TestMessaging) { + const intentRequest = input as RaiseIntentForContextRequest + const payload = intentRequest.payload + const context = payload?.context?.type + + if (m.getIntentResult() == undefined) { + // we're going to figure out the right response based on the app details (a la FindIntent) + const app = payload?.app + const using: IntentDetail = { + context, + app + } + + const relevant = m.intentDetails.filter(id => intentDetailMatches(id, using, false)) + const request = this.createRaiseIntentForContextResponseMessage(intentRequest, relevant, m) + setTimeout(() => { m.receive(request) }, 100) + } else if (!m.getIntentResult()?.timeout) { + // this sends out the pre-set intent resolution + const out1 = this.createCannedRaiseIntentForContextResponseMessage(intentRequest, m) + setTimeout(() => { m.receive(out1) }, 100) + + // next, send the result response + const out2 = this.createRaiseIntentResultResponseMesssage(intentRequest, m) + if (out2) { + setTimeout(() => { m.receive(out2) }, 300) + } + } + + return Promise.resolve() + } +} diff --git a/fdc3-for-web-implementation/packages/da-server/package.json b/fdc3-for-web-implementation/packages/da-server/package.json index afd6f6ce5..9a80ab5e3 100644 --- a/fdc3-for-web-implementation/packages/da-server/package.json +++ b/fdc3-for-web-implementation/packages/da-server/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/da-server", - "version": "0.0.50", + "version": "0.0.54", "files": [ "dist" ], @@ -21,8 +21,8 @@ "@cucumber/cucumber": "10.3.1", "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", - "@kite9/fdc3-common": "0.0.50", - "@kite9/testing": "0.0.50", + "@kite9/fdc3-common": "0.0.54", + "@kite9/testing": "0.0.54", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", diff --git a/fdc3-for-web-implementation/packages/demo/package.json b/fdc3-for-web-implementation/packages/demo/package.json index 9993814d1..a65cfa5e9 100644 --- a/fdc3-for-web-implementation/packages/demo/package.json +++ b/fdc3-for-web-implementation/packages/demo/package.json @@ -1,7 +1,7 @@ { "name": "@kite9/demo", "private": true, - "version": "0.0.50", + "version": "0.0.54", "scripts": { "dev": "nodemon -w src/server src/server/main.ts" }, @@ -14,9 +14,9 @@ }, "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", - "@kite9/client": "0.0.50", - "@kite9/da-server": "0.0.50", - "@kite9/fdc3-common": "0.0.50", + "@kite9/client": "0.0.54", + "@kite9/da-server": "0.0.54", + "@kite9/fdc3-common": "0.0.54", "@types/uuid": "^10.0.0", "@types/ws": "^8.5.10", "express": "^4.18.3", diff --git a/fdc3-for-web-implementation/packages/demo/src/client/apps/app7.ts b/fdc3-for-web-implementation/packages/demo/src/client/apps/app7.ts new file mode 100644 index 000000000..d98d14f95 --- /dev/null +++ b/fdc3-for-web-implementation/packages/demo/src/client/apps/app7.ts @@ -0,0 +1,19 @@ +import { getAgent } from '@kite9/client' + +/** + * This demonstrates using the API via a promise + */ +getAgent().then(async fdc3 => { + console.log("in promise") + const log = document.getElementById("log"); + const reso = await fdc3.raiseIntent("ViewNews", { + type: "fdc3.instrument", + id: { + isin: "Abc123" + } + }) + + log!!.textContent = `Got resolution: ${JSON.stringify(reso)}` + const result = await reso.getResult() + log!!.textContent += `Got result: ${JSON.stringify(result)}` +}); diff --git a/fdc3-for-web-implementation/packages/demo/src/client/da/dummy-desktop-agent.ts b/fdc3-for-web-implementation/packages/demo/src/client/da/dummy-desktop-agent.ts index 3285e6f2e..1515dcd31 100644 --- a/fdc3-for-web-implementation/packages/demo/src/client/da/dummy-desktop-agent.ts +++ b/fdc3-for-web-implementation/packages/demo/src/client/da/dummy-desktop-agent.ts @@ -1,11 +1,12 @@ import { io } from "socket.io-client" import { v4 as uuid } from 'uuid' -import { APP_GOODBYE, DA_HELLO, FDC3_APP_EVENT } from "../../message-types"; +import { APP_GOODBYE, APP_HELLO, DA_HELLO, FDC3_APP_EVENT } from "../../message-types"; import { DemoServerContext } from "./DemoServerContext"; import { FDC3_2_1_JSONDirectory } from "./FDC3_2_1_JSONDirectory"; import { DefaultFDC3Server, DirectoryApp, ServerContext } from "@kite9/da-server"; import { WebConnectionProtocol2LoadURL } from "@kite9/fdc3-common"; import { ChannelState, ChannelType } from "@kite9/da-server/src/handlers/BroadcastHandler"; +import { link } from "./util"; function createAppStartButton(app: DirectoryApp, sc: ServerContext): HTMLDivElement { @@ -53,8 +54,8 @@ window.addEventListener("load", () => { socket.emit(DA_HELLO, desktopAgentUUID) const directory = new FDC3_2_1_JSONDirectory() - //await directory.load("/static/da/appd.json") - await directory.load("/static/da/local-conformance-2_0.v2.json") + await directory.load("/static/da/appd.json") + //await directory.load("/static/da/local-conformance-2_0.v2.json") const sc = new DemoServerContext(socket, directory) const channelDetails: ChannelState[] = [ @@ -102,22 +103,25 @@ window.addEventListener("load", () => { } } as WebConnectionProtocol2LoadURL, origin) } else { - // const details = buildConnection() - // details.context.setInstanceDetails('uuid', { appId: 'Test App Id', instanceId: '1' }) - // connections.push(details) - - // source.postMessage({ - // type: "WCP3Handshake", - // meta: { - // connectionAttemptUuid: data.meta.connectionAttemptUuid, - // timestamp: new Date() - // }, - // payload: { - // fdc3Version: "2.2", - // resolver: window.location.origin + "/static/da/intent-resolver.html", - // channelSelector: window.location.origin + "/static/da/channel-selector.html", - // } - // } as WebConnectionProtocol3Handshake, origin, [details.externalPort]) + const instance = sc.getInstanceForWindow(source)!! + const channel = new MessageChannel() + link(socket, channel, instance.instanceId!!) + + socket.emit(APP_HELLO, desktopAgentUUID, instance.instanceId) + + // sned the other end of the channel to the app + source.postMessage({ + type: 'WCP3Handshake', + meta: { + connectionAttemptUuid: data.meta.connectionAttemptUuid, + timestamp: new Date() + }, + payload: { + fdc3Version: "2.2", + resolver: window.location.origin + "/static/da/intent-resolver.html", + channelSelector: window.location.origin + "/static/da/channel-selector.html", + } + }, origin, [channel.port1]) } } }); diff --git a/fdc3-for-web-implementation/packages/demo/src/client/da/embed.ts b/fdc3-for-web-implementation/packages/demo/src/client/da/embed.ts index ee9e49af5..671dace45 100644 --- a/fdc3-for-web-implementation/packages/demo/src/client/da/embed.ts +++ b/fdc3-for-web-implementation/packages/demo/src/client/da/embed.ts @@ -1,6 +1,5 @@ import { io } from "socket.io-client" import { link } from "./util"; -import { AppIdentifier } from "@finos/fdc3"; import { APP_HELLO } from "../../message-types"; const appWindow = window.parent; diff --git a/fdc3-for-web-implementation/packages/demo/src/client/ui/channel-selector.ts b/fdc3-for-web-implementation/packages/demo/src/client/ui/channel-selector.ts index 8fed647f6..49179c682 100644 --- a/fdc3-for-web-implementation/packages/demo/src/client/ui/channel-selector.ts +++ b/fdc3-for-web-implementation/packages/demo/src/client/ui/channel-selector.ts @@ -1,9 +1,29 @@ -import { ChannelDetails, SelectorMessageChannels } from "@kite9/fdc3-common"; +import { IframeChannels } from "@kite9/fdc3-common"; -var channels: ChannelDetails[] = [] +var channels: any[] = [] var channelId: string | null = null +const DEFAULT_COLLAPSED_CSS = { + position: "fixed", + zIndex: "1000", + right: "10px", + bottom: "10px", + width: "50px", + height: "50px" +} + +const DEFAULT_EXPANDED_CSS = { + position: "fixed", + zIndex: "1000", + right: "10px", + bottom: "10px", + width: "450px", + maxHeight: "600px", + transition: "all 0.5s ease-out allow-discrete" +} + + window.addEventListener("load", () => { const parent = window.parent; const logo = document.getElementById("logo")!! @@ -14,19 +34,23 @@ window.addEventListener("load", () => { const myPort = mc.port1 myPort.start() - parent.postMessage({ type: "SelectorMessageInitialize" }, "*", [mc.port2]); + parent.postMessage({ type: "iframeHello" }, "*", [mc.port2]); function changeSize(expanded: boolean) { document.body.setAttribute("data-expanded", "" + expanded); - myPort.postMessage({ type: "SelectorMessageResize", expanded }) + myPort.postMessage({ type: "iframeRestyle", payload: { css: expanded ? DEFAULT_EXPANDED_CSS : DEFAULT_COLLAPSED_CSS } }) } myPort.addEventListener("message", (e) => { - if (e.data.type == 'SelectorMessageChannels') { - const details = e.data as SelectorMessageChannels + console.log(e.data.type) + if (e.data.type == 'iframeHandshake') { + // ok, port is ready, send the iframe position detials + myPort.postMessage({ type: "iframeRestyle", payload: { css: DEFAULT_COLLAPSED_CSS } }) + } else if (e.data.type == 'iframeChannels') { + const details = e.data as IframeChannels console.log(JSON.stringify("CHANNEL DETAILS: " + JSON.stringify(details))) - channels = details.channels - channelId = details.selected + channels = details.payload.userChannels + channelId = details.payload.selected const selectedColor = (channelId ? (channels.find(c => c.id == channelId)?.displayMetadata?.color) : null) ?? 'white' logo.style.backgroundColor = selectedColor @@ -50,7 +74,8 @@ window.addEventListener("load", () => { a.setAttribute("href", "#") a.onclick = () => { changeSize(false) - myPort.postMessage({ type: "SelectorMessageChoice", channelId: channel.id }) + channelId = channel.id + myPort.postMessage({ type: "iframeChannelSelected", payload: { selected: channel.id } }) } }) diff --git a/fdc3-for-web-implementation/packages/demo/src/client/ui/intent-resolver.ts b/fdc3-for-web-implementation/packages/demo/src/client/ui/intent-resolver.ts index 3fe44232e..f892535b6 100644 --- a/fdc3-for-web-implementation/packages/demo/src/client/ui/intent-resolver.ts +++ b/fdc3-for-web-implementation/packages/demo/src/client/ui/intent-resolver.ts @@ -1,5 +1,22 @@ -import { ResolverIntents, ResolverMessageChoice, SingleAppIntent } from "@kite9/fdc3-common"; +import { AppIdentifier, IframeResolveAction, IframeResolvePayload } from "@kite9/fdc3-common"; +const DEFAULT_COLLAPSED_CSS = { + position: "fixed", + zIndex: "1000", + right: "0", + bottom: "0", + width: "0", + height: "0" +} + +const DEFAULT_EXPANDED_CSS = { + position: "fixed", + zIndex: "1000", + left: "10%", + top: "10%", + right: "10%", + bottom: "10%" +} window.addEventListener("load", () => { const parent = window.parent; @@ -10,20 +27,37 @@ window.addEventListener("load", () => { const list = document.getElementById("intent-list")!! - parent.postMessage({ type: "SelectorMessageInitialize" }, "*", [mc.port2]); + parent.postMessage({ type: "iframeHello" }, "*", [mc.port2]); + + function callback(intent: string | null, app: AppIdentifier | null) { + myPort.postMessage({ type: "iframeRestyle", payload: { css: DEFAULT_COLLAPSED_CSS } }) - function callback(si: SingleAppIntent | null) { - myPort.postMessage({ - type: "ResolverMessageChoice", - payload: si - } as ResolverMessageChoice) + if (intent && app) { + myPort.postMessage({ + type: "iframeResolveAction", + payload: { + action: "click", + appIdentifier: app, + intent: intent + } + } as IframeResolveAction) + } else { + myPort.postMessage({ + type: "iframeResolveAction", + payload: { + action: "cancel" + } + } as IframeResolveAction) + } } myPort.addEventListener("message", (e) => { - if (e.data.type == 'ResolverIntents') { - const details = e.data as ResolverIntents - console.log(JSON.stringify("INTENT DETAILS: " + JSON.stringify(details))) + if (e.data.type == 'iframeHandshake') { + myPort.postMessage({ type: "iframeRestyle", payload: { css: DEFAULT_COLLAPSED_CSS } }) + } else if (e.data.type == 'iframeResolve') { + myPort.postMessage({ type: "iframeRestyle", payload: { css: DEFAULT_EXPANDED_CSS } }) + const details = e.data.payload as IframeResolvePayload details.appIntents.forEach(intent => { intent.apps.forEach(app => { @@ -43,14 +77,14 @@ window.addEventListener("load", () => { li.appendChild(description) list.appendChild(li) a.setAttribute("href", "#") - a.onclick = () => callback({ intent: intent.intent, chosenApp: app }) + a.onclick = () => callback(intent.intent.name, app) }) }) } }) document.getElementById("cancel")!!.addEventListener("click", () => { - callback(null); + callback(null, null); }) diff --git a/fdc3-for-web-implementation/packages/demo/static/app7/index.html b/fdc3-for-web-implementation/packages/demo/static/app7/index.html new file mode 100644 index 000000000..a50a907b1 --- /dev/null +++ b/fdc3-for-web-implementation/packages/demo/static/app7/index.html @@ -0,0 +1,15 @@ + + + + App 7 + + + + + +

FDC3 For the Web App7

+

Asks for an intent result, involves intent resolver

+
+ + + \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/demo/static/da/appd.json b/fdc3-for-web-implementation/packages/demo/static/da/appd.json index e3dcbc1ec..8de2616c5 100644 --- a/fdc3-for-web-implementation/packages/demo/static/da/appd.json +++ b/fdc3-for-web-implementation/packages/demo/static/da/appd.json @@ -128,6 +128,20 @@ "publisher": "FINOS", "icons": [] }, + { + "appId": "app7", + "name": "App Seven", + "title": "Intent Result 2", + "description": "App asks for the result of a ViewNews intent", + "type": "web", + "details": { + "url": "http://localhost:8095/static/app7/index.html" + }, + "hostManifests": {}, + "version": "1.0.0", + "publisher": "FINOS", + "icons": [] + }, { "appId": "grid", "name": "grid", diff --git a/fdc3-for-web-implementation/packages/fdc3-common/package.json b/fdc3-for-web-implementation/packages/fdc3-common/package.json index 5eeee0c0c..ab9a6aa8d 100644 --- a/fdc3-for-web-implementation/packages/fdc3-common/package.json +++ b/fdc3-for-web-implementation/packages/fdc3-common/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/fdc3-common", - "version": "0.0.50", + "version": "0.0.54", "files": [ "dist" ], diff --git a/fdc3-for-web-implementation/packages/fdc3-common/src/BrowserTypes.ts b/fdc3-for-web-implementation/packages/fdc3-common/src/BrowserTypes.ts index f472277ac..44fe32e86 100644 --- a/fdc3-for-web-implementation/packages/fdc3-common/src/BrowserTypes.ts +++ b/fdc3-for-web-implementation/packages/fdc3-common/src/BrowserTypes.ts @@ -1,6 +1,6 @@ // To parse this data: // -// import { Convert, AddContextListenerRequest, AddContextListenerResponse, AddEventListenerEvent, AddEventListenerRequest, AddEventListenerResponse, AddIntentListenerRequest, AddIntentListenerResponse, AgentEventMessage, AgentResponseMessage, AppRequestMessage, BroadcastEvent, BroadcastRequest, BroadcastResponse, ChannelChangedEvent, ContextListenerUnsubscribeRequest, ContextListenerUnsubscribeResponse, CreatePrivateChannelRequest, CreatePrivateChannelResponse, FindInstancesRequest, FindInstancesResponse, FindIntentRequest, FindIntentResponse, FindIntentsByContextRequest, FindIntentsByContextResponse, GetAppMetadataRequest, GetAppMetadataResponse, GetCurrentChannelRequest, GetCurrentChannelResponse, GetCurrentContextRequest, GetCurrentContextResponse, GetInfoRequest, GetInfoResponse, GetOrCreateChannelRequest, GetOrCreateChannelResponse, GetUserChannelsRequest, GetUserChannelsResponse, IframeChannelDrag, IframeChannelResize, IframeChannels, IframeChannelSelected, IframeHandshake, IframeHello, IframeMessage, IframeResolve, IframeResolveAction, IntentEvent, IntentListenerUnsubscribeRequest, IntentListenerUnsubscribeResponse, IntentResultRequest, IntentResultResponse, JoinUserChannelRequest, JoinUserChannelResponse, LeaveCurrentChannelRequest, LeaveCurrentChannelResponse, OpenRequest, OpenResponse, PrivateChannelAddEventListenerRequest, PrivateChannelAddEventListenerResponse, PrivateChannelDisconnectRequest, PrivateChannelDisconnectResponse, PrivateChannelOnAddContextListenerEvent, PrivateChannelOnDisconnectEvent, PrivateChannelOnUnsubscribeEvent, PrivateChannelUnsubscribeEventListenerRequest, PrivateChannelUnsubscribeEventListenerResponse, RaiseIntentForContextRequest, RaiseIntentForContextResponse, RaiseIntentRequest, RaiseIntentResponse, RaiseIntentResultResponse, WebConnectionProtocol1Hello, WebConnectionProtocol2LoadURL, WebConnectionProtocol3Handshake, WebConnectionProtocol4ValidateAppIdentity, WebConnectionProtocol5ValidateAppIdentityFailedResponse, WebConnectionProtocol5ValidateAppIdentitySuccessResponse, WebConnectionProtocolMessage } from "./file"; +// import { Convert, AddContextListenerRequest, AddContextListenerResponse, AddEventListenerEvent, AddEventListenerRequest, AddEventListenerResponse, AddIntentListenerRequest, AddIntentListenerResponse, AgentEventMessage, AgentResponseMessage, AppRequestMessage, BroadcastEvent, BroadcastRequest, BroadcastResponse, ChannelChangedEvent, ContextListenerUnsubscribeRequest, ContextListenerUnsubscribeResponse, CreatePrivateChannelRequest, CreatePrivateChannelResponse, EventListenerUnsubscribeRequest, EventListenerUnsubscribeResponse, FindInstancesRequest, FindInstancesResponse, FindIntentRequest, FindIntentResponse, FindIntentsByContextRequest, FindIntentsByContextResponse, GetAppMetadataRequest, GetAppMetadataResponse, GetCurrentChannelRequest, GetCurrentChannelResponse, GetCurrentContextRequest, GetCurrentContextResponse, GetInfoRequest, GetInfoResponse, GetOrCreateChannelRequest, GetOrCreateChannelResponse, GetUserChannelsRequest, GetUserChannelsResponse, IframeChannelDrag, IframeChannelResize, IframeChannels, IframeChannelSelected, IframeHandshake, IframeHello, IframeMessage, IframeResolve, IframeResolveAction, IntentEvent, IntentListenerUnsubscribeRequest, IntentListenerUnsubscribeResponse, IntentResultRequest, IntentResultResponse, JoinUserChannelRequest, JoinUserChannelResponse, LeaveCurrentChannelRequest, LeaveCurrentChannelResponse, OpenRequest, OpenResponse, PrivateChannelAddEventListenerRequest, PrivateChannelAddEventListenerResponse, PrivateChannelDisconnectRequest, PrivateChannelDisconnectResponse, PrivateChannelOnAddContextListenerEvent, PrivateChannelOnDisconnectEvent, PrivateChannelOnUnsubscribeEvent, PrivateChannelUnsubscribeEventListenerRequest, PrivateChannelUnsubscribeEventListenerResponse, RaiseIntentForContextRequest, RaiseIntentForContextResponse, RaiseIntentRequest, RaiseIntentResponse, RaiseIntentResultResponse, WebConnectionProtocol1Hello, WebConnectionProtocol2LoadURL, WebConnectionProtocol3Handshake, WebConnectionProtocol4ValidateAppIdentity, WebConnectionProtocol5ValidateAppIdentityFailedResponse, WebConnectionProtocol5ValidateAppIdentitySuccessResponse, WebConnectionProtocol6Goodbye, WebConnectionProtocolMessage } from "./file"; // // const addContextListenerRequest = Convert.toAddContextListenerRequest(json); // const addContextListenerResponse = Convert.toAddContextListenerResponse(json); @@ -20,6 +20,8 @@ // const contextListenerUnsubscribeResponse = Convert.toContextListenerUnsubscribeResponse(json); // const createPrivateChannelRequest = Convert.toCreatePrivateChannelRequest(json); // const createPrivateChannelResponse = Convert.toCreatePrivateChannelResponse(json); +// const eventListenerUnsubscribeRequest = Convert.toEventListenerUnsubscribeRequest(json); +// const eventListenerUnsubscribeResponse = Convert.toEventListenerUnsubscribeResponse(json); // const findInstancesRequest = Convert.toFindInstancesRequest(json); // const findInstancesResponse = Convert.toFindInstancesResponse(json); // const findIntentRequest = Convert.toFindIntentRequest(json); @@ -78,6 +80,7 @@ // const webConnectionProtocol4ValidateAppIdentity = Convert.toWebConnectionProtocol4ValidateAppIdentity(json); // const webConnectionProtocol5ValidateAppIdentityFailedResponse = Convert.toWebConnectionProtocol5ValidateAppIdentityFailedResponse(json); // const webConnectionProtocol5ValidateAppIdentitySuccessResponse = Convert.toWebConnectionProtocol5ValidateAppIdentitySuccessResponse(json); +// const webConnectionProtocol6Goodbye = Convert.toWebConnectionProtocol6Goodbye(json); // const webConnectionProtocolMessage = Convert.toWebConnectionProtocolMessage(json); // // These functions will throw an error if the JSON doesn't @@ -573,7 +576,7 @@ export interface AgentResponseMessageResponsePayload { * Identifies the type of the message and it is typically set to the FDC3 function name that * the message relates to, e.g. 'findIntent', with 'Response' appended. */ -export type ResponseMessageType = "addContextListenerResponse" | "addEventListenerResponse" | "addIntentListenerResponse" | "broadcastResponse" | "contextListenerUnsubscribeResponse" | "createPrivateChannelResponse" | "findInstancesResponse" | "findIntentResponse" | "findIntentsByContextResponse" | "getAppMetadataResponse" | "getCurrentChannelResponse" | "getCurrentContextResponse" | "getInfoResponse" | "getOrCreateChannelResponse" | "getUserChannelsResponse" | "intentListenerUnsubscribeResponse" | "intentResultResponse" | "joinUserChannelResponse" | "leaveCurrentChannelResponse" | "openResponse" | "privateChannelAddEventListenerResponse" | "privateChannelDisconnectResponse" | "privateChannelUnsubscribeEventListenerResponse" | "raiseIntentForContextResponse" | "raiseIntentResponse" | "raiseIntentResultResponse"; +export type ResponseMessageType = "addContextListenerResponse" | "addEventListenerResponse" | "addIntentListenerResponse" | "broadcastResponse" | "contextListenerUnsubscribeResponse" | "createPrivateChannelResponse" | "eventListenerUnsubscribeResponse" | "findInstancesResponse" | "findIntentResponse" | "findIntentsByContextResponse" | "getAppMetadataResponse" | "getCurrentChannelResponse" | "getCurrentContextResponse" | "getInfoResponse" | "getOrCreateChannelResponse" | "getUserChannelsResponse" | "intentListenerUnsubscribeResponse" | "intentResultResponse" | "joinUserChannelResponse" | "leaveCurrentChannelResponse" | "openResponse" | "privateChannelAddEventListenerResponse" | "privateChannelDisconnectResponse" | "privateChannelUnsubscribeEventListenerResponse" | "raiseIntentForContextResponse" | "raiseIntentResponse" | "raiseIntentResultResponse"; /** * A request message from an FDC3-enabled app to a Desktop Agent. @@ -613,11 +616,12 @@ export interface AppRequestMessageMeta { * Identifies the type of the message and it is typically set to the FDC3 function name that * the message relates to, e.g. 'findIntent', with 'Request' appended. */ -export type RequestMessageType = "addContextListenerRequest" | "addEventListenerRequest" | "addIntentListenerRequest" | "broadcastRequest" | "contextListenerUnsubscribeRequest" | "createPrivateChannelRequest" | "findInstancesRequest" | "findIntentRequest" | "findIntentsByContextRequest" | "getAppMetadataRequest" | "getCurrentChannelRequest" | "getCurrentContextRequest" | "getInfoRequest" | "getOrCreateChannelRequest" | "getUserChannelsRequest" | "intentListenerUnsubscribeRequest" | "intentResultRequest" | "joinUserChannelRequest" | "leaveCurrentChannelRequest" | "openRequest" | "privateChannelAddEventListenerRequest" | "privateChannelDisconnectRequest" | "privateChannelUnsubscribeEventListenerRequest" | "raiseIntentForContextRequest" | "raiseIntentRequest"; +export type RequestMessageType = "addContextListenerRequest" | "addEventListenerRequest" | "addIntentListenerRequest" | "broadcastRequest" | "contextListenerUnsubscribeRequest" | "createPrivateChannelRequest" | "eventListenerUnsubscribeRequest" | "findInstancesRequest" | "findIntentRequest" | "findIntentsByContextRequest" | "getAppMetadataRequest" | "getCurrentChannelRequest" | "getCurrentContextRequest" | "getInfoRequest" | "getOrCreateChannelRequest" | "getUserChannelsRequest" | "intentListenerUnsubscribeRequest" | "intentResultRequest" | "joinUserChannelRequest" | "leaveCurrentChannelRequest" | "openRequest" | "privateChannelAddEventListenerRequest" | "privateChannelDisconnectRequest" | "privateChannelUnsubscribeEventListenerRequest" | "raiseIntentForContextRequest" | "raiseIntentRequest"; /** * An event message from the Desktop Agent to an app indicating that context has been - * broadcast on a channel it is listening to. + * broadcast on a channel it is listening to, or specifically to this app instance if it was + * launched via `fdc3.open` and context was passed. * * A message from a Desktop Agent to an FDC3-enabled app representing an event. */ @@ -642,9 +646,10 @@ export interface BroadcastEvent { */ export interface BroadcastEventPayload { /** - * The Id of the channel that the broadcast was sent on. + * The Id of the channel that the broadcast was sent on. May be `null` if the context is + * being broadcast due to a call `fdc3.open` that passed context. */ - channelId: string; + channelId: null | string; /** * The context object that was broadcast. */ @@ -875,7 +880,7 @@ export interface ContextListenerUnsubscribeRequestPayload { */ /** - * A response to a request to a contextListenerUnsubscribe request. + * A response to a contextListenerUnsubscribe request. * * A message from a Desktop Agent to an FDC3-enabled app responding to an API call. If the * payload contains an `error` property, the request was unsuccessful. @@ -1035,6 +1040,68 @@ export interface DisplayMetadata { */ export type Type = "app" | "private" | "user"; +/** + * Identifies the type of the message and it is typically set to the FDC3 function name that + * the message relates to, e.g. 'findIntent', with 'Response' appended. + */ + +/** + * A request to unsubscribe an event listener. + * + * A request message from an FDC3-enabled app to a Desktop Agent. + */ +export interface EventListenerUnsubscribeRequest { + /** + * Metadata for a request message sent by an FDC3-enabled app to a Desktop Agent. + */ + meta: AddContextListenerRequestMeta; + /** + * The message payload typically contains the arguments to FDC3 API functions. + */ + payload: EventListenerUnsubscribeRequestPayload; + /** + * Identifies the type of the message and it is typically set to the FDC3 function name that + * the message relates to, e.g. 'findIntent', with 'Request' appended. + */ + type: "eventListenerUnsubscribeRequest"; +} + +/** + * The message payload typically contains the arguments to FDC3 API functions. + */ +export interface EventListenerUnsubscribeRequestPayload { + listenerUUID: string; +} + +/** + * Identifies the type of the message and it is typically set to the FDC3 function name that + * the message relates to, e.g. 'findIntent', with 'Request' appended. + */ + +/** + * A response to an eventListenerUnsubscribe request. + * + * A message from a Desktop Agent to an FDC3-enabled app responding to an API call. If the + * payload contains an `error` property, the request was unsuccessful. + */ +export interface EventListenerUnsubscribeResponse { + /** + * Metadata for messages sent by a Desktop Agent to an App in response to an API call + */ + meta: AddContextListenerResponseMeta; + /** + * A payload for a response to an API call that will contain any return values or an `error` + * property containing a standardized error message indicating that the request was + * unsuccessful. + */ + payload: BroadcastResponseResponsePayload; + /** + * Identifies the type of the message and it is typically set to the FDC3 function name that + * the message relates to, e.g. 'findIntent', with 'Response' appended. + */ + type: "eventListenerUnsubscribeResponse"; +} + /** * Identifies the type of the message and it is typically set to the FDC3 function name that * the message relates to, e.g. 'findIntent', with 'Response' appended. @@ -2373,7 +2440,7 @@ export interface IntentListenerUnsubscribeRequestPayload { */ /** - * A response to a request to a intentListenerUnsubscribe request. + * A response to a intentListenerUnsubscribe request. * * A message from a Desktop Agent to an FDC3-enabled app responding to an API call. If the * payload contains an `error` property, the request was unsuccessful. @@ -3583,6 +3650,31 @@ export interface WebConnectionProtocol5ValidateAppIdentitySuccessResponsePayload instanceUuid: string; } +/** + * Identifies the type of the connection step message. + */ + +/** + * Goodbye message to be sent to the Desktop Agent when disconnecting (e.g. when closing the + * window or navigating). Desktop Agents should close the MessagePort after receiving this + * message, but retain instance details in case the application reconnects (e.g. after a + * navigation event). + * + * A message used during the connection flow for an application to a Desktop Agent in a + * browser window. Used for messages sent in either direction. + */ +export interface WebConnectionProtocol6Goodbye { + meta: ConnectionStepMetadata; + /** + * The message payload, containing data pertaining to this connection step. + */ + payload: { [key: string]: any }; + /** + * Identifies the type of the connection step message. + */ + type: "WCP6Goodbye"; +} + /** * Identifies the type of the connection step message. */ @@ -3606,7 +3698,7 @@ export interface WebConnectionProtocolMessage { /** * Identifies the type of the connection step message. */ -export type ConnectionStepMessageType = "WCP1Hello" | "WCP2LoadUrl" | "WCP3Handshake" | "WCP4ValidateAppIdentity" | "WCP5ValidateAppIdentityFailedResponse" | "WCP5ValidateAppIdentityResponse"; +export type ConnectionStepMessageType = "WCP1Hello" | "WCP2LoadUrl" | "WCP3Handshake" | "WCP4ValidateAppIdentity" | "WCP5ValidateAppIdentityFailedResponse" | "WCP5ValidateAppIdentityResponse" | "WCP6Goodbye"; // Converts JSON strings to/from your types // and asserts the results of JSON.parse at runtime @@ -3755,6 +3847,22 @@ export class Convert { return JSON.stringify(uncast(value, r("CreatePrivateChannelResponse")), null, 2); } + public static toEventListenerUnsubscribeRequest(json: string): EventListenerUnsubscribeRequest { + return cast(JSON.parse(json), r("EventListenerUnsubscribeRequest")); + } + + public static eventListenerUnsubscribeRequestToJson(value: EventListenerUnsubscribeRequest): string { + return JSON.stringify(uncast(value, r("EventListenerUnsubscribeRequest")), null, 2); + } + + public static toEventListenerUnsubscribeResponse(json: string): EventListenerUnsubscribeResponse { + return cast(JSON.parse(json), r("EventListenerUnsubscribeResponse")); + } + + public static eventListenerUnsubscribeResponseToJson(value: EventListenerUnsubscribeResponse): string { + return JSON.stringify(uncast(value, r("EventListenerUnsubscribeResponse")), null, 2); + } + public static toFindInstancesRequest(json: string): FindInstancesRequest { return cast(JSON.parse(json), r("FindInstancesRequest")); } @@ -4219,6 +4327,14 @@ export class Convert { return JSON.stringify(uncast(value, r("WebConnectionProtocol5ValidateAppIdentitySuccessResponse")), null, 2); } + public static toWebConnectionProtocol6Goodbye(json: string): WebConnectionProtocol6Goodbye { + return cast(JSON.parse(json), r("WebConnectionProtocol6Goodbye")); + } + + public static webConnectionProtocol6GoodbyeToJson(value: WebConnectionProtocol6Goodbye): string { + return JSON.stringify(uncast(value, r("WebConnectionProtocol6Goodbye")), null, 2); + } + public static toWebConnectionProtocolMessage(json: string): WebConnectionProtocolMessage { return cast(JSON.parse(json), r("WebConnectionProtocolMessage")); } @@ -4504,7 +4620,7 @@ const typeMap: any = { { json: "type", js: "type", typ: r("BroadcastEventType") }, ], false), "BroadcastEventPayload": o([ - { json: "channelId", js: "channelId", typ: "" }, + { json: "channelId", js: "channelId", typ: u(null, "") }, { json: "context", js: "context", typ: r("Context") }, { json: "originatingApp", js: "originatingApp", typ: u(undefined, r("AppIdentifier")) }, ], false), @@ -4577,6 +4693,19 @@ const typeMap: any = { { json: "glyph", js: "glyph", typ: u(undefined, "") }, { json: "name", js: "name", typ: u(undefined, "") }, ], false), + "EventListenerUnsubscribeRequest": o([ + { json: "meta", js: "meta", typ: r("AddContextListenerRequestMeta") }, + { json: "payload", js: "payload", typ: r("EventListenerUnsubscribeRequestPayload") }, + { json: "type", js: "type", typ: r("EventListenerUnsubscribeRequestType") }, + ], false), + "EventListenerUnsubscribeRequestPayload": o([ + { json: "listenerUUID", js: "listenerUUID", typ: "" }, + ], false), + "EventListenerUnsubscribeResponse": o([ + { json: "meta", js: "meta", typ: r("AddContextListenerResponseMeta") }, + { json: "payload", js: "payload", typ: r("BroadcastResponseResponsePayload") }, + { json: "type", js: "type", typ: r("EventListenerUnsubscribeResponseType") }, + ], false), "FindInstancesRequest": o([ { json: "meta", js: "meta", typ: r("AddContextListenerRequestMeta") }, { json: "payload", js: "payload", typ: r("FindInstancesRequestPayload") }, @@ -5134,6 +5263,11 @@ const typeMap: any = { { json: "instanceId", js: "instanceId", typ: "" }, { json: "instanceUuid", js: "instanceUuid", typ: "" }, ], false), + "WebConnectionProtocol6Goodbye": o([ + { json: "meta", js: "meta", typ: r("ConnectionStepMetadata") }, + { json: "payload", js: "payload", typ: m("any") }, + { json: "type", js: "type", typ: r("WebConnectionProtocol6GoodbyeType") }, + ], false), "WebConnectionProtocolMessage": o([ { json: "meta", js: "meta", typ: r("ConnectionStepMetadata") }, { json: "payload", js: "payload", typ: m("any") }, @@ -5219,6 +5353,7 @@ const typeMap: any = { "broadcastResponse", "contextListenerUnsubscribeResponse", "createPrivateChannelResponse", + "eventListenerUnsubscribeResponse", "findInstancesResponse", "findIntentResponse", "findIntentsByContextResponse", @@ -5247,6 +5382,7 @@ const typeMap: any = { "broadcastRequest", "contextListenerUnsubscribeRequest", "createPrivateChannelRequest", + "eventListenerUnsubscribeRequest", "findInstancesRequest", "findIntentRequest", "findIntentsByContextRequest", @@ -5296,6 +5432,12 @@ const typeMap: any = { "CreatePrivateChannelResponseType": [ "createPrivateChannelResponse", ], + "EventListenerUnsubscribeRequestType": [ + "eventListenerUnsubscribeRequest", + ], + "EventListenerUnsubscribeResponseType": [ + "eventListenerUnsubscribeResponse", + ], "FindInstancesRequestType": [ "findInstancesRequest", ], @@ -5525,6 +5667,9 @@ const typeMap: any = { "WebConnectionProtocol5ValidateAppIdentitySuccessResponseType": [ "WCP5ValidateAppIdentityResponse", ], + "WebConnectionProtocol6GoodbyeType": [ + "WCP6Goodbye", + ], "ConnectionStepMessageType": [ "WCP1Hello", "WCP2LoadUrl", @@ -5532,5 +5677,6 @@ const typeMap: any = { "WCP4ValidateAppIdentity", "WCP5ValidateAppIdentityFailedResponse", "WCP5ValidateAppIdentityResponse", + "WCP6Goodbye", ], -}; \ No newline at end of file +}; diff --git a/fdc3-for-web-implementation/packages/fdc3-common/src/ChannelSelector.ts b/fdc3-for-web-implementation/packages/fdc3-common/src/ChannelSelector.ts new file mode 100644 index 000000000..75cc1b00f --- /dev/null +++ b/fdc3-for-web-implementation/packages/fdc3-common/src/ChannelSelector.ts @@ -0,0 +1,24 @@ +import { Channel } from "@finos/fdc3" + +/** + * Interface used by the desktop agent proxy to handle the channel selection process. + */ +export interface ChannelSelector { + + /** + * Make sure the channel selector is ready to be used. + */ + init(): Promise + + /** + * Called when the list of user channels is updated, or the selected channel changes. + */ + updateChannel(channelId: string | null, availableChannels: Channel[]): void + + /** + * Called on initialisation. The channel selector will invoke the callback after the + * channel is changed. + */ + setChannelChangeCallback(callback: (channelId: string) => void): void + +} diff --git a/fdc3-for-web-implementation/packages/fdc3-common/src/IntentResolver.ts b/fdc3-for-web-implementation/packages/fdc3-common/src/IntentResolver.ts new file mode 100644 index 000000000..60f1b80a4 --- /dev/null +++ b/fdc3-for-web-implementation/packages/fdc3-common/src/IntentResolver.ts @@ -0,0 +1,25 @@ +import { AppIdentifier, AppIntent, Context } from "@finos/fdc3"; + + +export type IntentResolutionChoice = { + intent: string, + appId: AppIdentifier +} + +/** + * Interface used by the desktop agent proxy to handle the intent resolution process. + */ +export interface IntentResolver { + + /** + * Make sure the intent resolver is ready to be used. + */ + init(): Promise + + /** + * Called when the user needs to resolve an intent. Returns either the app chosen to + * resolve the intent or void if the operation was cancelled. + */ + chooseIntent(appIntents: AppIntent[], context: Context): Promise +} + diff --git a/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts b/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts index 505a5a9ac..f50c813c9 100644 --- a/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts +++ b/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts @@ -1,173 +1,18 @@ -import { AppIntent, AppIdentifier, DesktopAgent, IntentMetadata, IntentResult, Channel } from "@finos/fdc3"; -import { WebConnectionProtocol1Hello, WebConnectionProtocol2LoadURL, WebConnectionProtocol3Handshake } from "./BrowserTypes"; +import { + AppIdentifier, DesktopAgent +} from "@finos/fdc3"; import { GetAgentParams } from "./GetAgent"; export type AppChecker = (o: Window) => AppIdentifier | undefined; -export type Supplier = ( - checker: AppChecker, - detailsResolver: DesktopAgentDetailResolver, - portResolver?: DesktopAgentPortResolver, - on?: Window) => void; - export type Loader = (options: GetAgentParams) => Promise -/** - * Use these to return details specific to the window/app needing a connection - */ -export type DesktopAgentDetailResolver = (o: Window, a: WebConnectionProtocol1Hello) => WebConnectionProtocol3Handshake | WebConnectionProtocol2LoadURL - -/** - * Same as above, but for the port - */ -export type DesktopAgentPortResolver = (o: Window, a: WebConnectionProtocol1Hello) => MessagePort | null - -export interface CSSPositioning { [key: string]: string } - -export const CSS_ELEMENTS = ["width", - "height", - "position", - "zIndex", - "left", - "right", - "top", - "bottom", - "transition", - "maxHeight", - "maxWidth"] - -export type ChannelSelectorDetails = { - uri?: string, - expandedCss?: CSSPositioning, - collapsedCss?: CSSPositioning -} - -export type IntentResolverDetails = { - uri?: string, - css?: CSSPositioning -} - -/** - * Contains the details of a single intent and application resolved - * by the IntentResolver implementation - */ -export interface SingleAppIntent { - - intent: IntentMetadata - chosenApp: AppIdentifier - -} - -/** - * Interface used by the desktop agent proxy to handle the channel selection process. - */ -export interface ChannelSelector { - - updateChannel(channelId: string | null, availableChannels: Channel[]): void - - setChannelChangeCallback(callback: (channelId: string) => void): void - -} - -/** - * Interface used by the desktop agent proxy to handle the intent resolution process. - */ -export interface IntentResolver { - - /** - * Called when the user needs to resolve an intent. - */ - chooseIntent(appIntents: AppIntent[], source: AppIdentifier): Promise - - /** - * Steps after the user has chosen the intent - */ - intentChosen(intentResult: IntentResult): Promise - -} - - -export type IntentResolutionChoiceAgentResponse = { - type: 'intentResolutionChoice', - payload: SingleAppIntent -} - - -export type IntentResolutionChoiceAgentRequest = IntentResolutionChoiceAgentResponse - - -export type ChannelSelectionChoiceAgentRequest = { - type: 'channelSelectionChoice', - payload: { - channelId: string, - cancelled: boolean, - } -} - -export type ChannelSelectionChoiceAgentResponse = ChannelSelectionChoiceAgentRequest - -/** - * Messages from the app to the channel selector / intent resolver - */ -export type ChannelDetails = { - id: string; - displayMetadata: { - name: string; - color: string; - glyph?: string; - } -}; - -export type SelectorMessageChannels = { - type: "SelectorMessageChannels"; - channels: ChannelDetails[]; - selected: string; -} - - -/** - * From the channel selector to the app - */ -export type SelectorMessageChoice = { - type: "SelectorMessageChoice"; - channelId: string | null; -} - -/** - * From the channel selector to the app - */ -export type SelectorMessageResize = { - type: "SelectorMessageResize"; - expanded: boolean; -} - -/** - * From the channel selector/intent resolver to the app, on startup - */ -export type SelectorMessageInitialize = { - type: "SelectorMessageInitialize" -} - -/** - * From the intent resolver to the app - */ -export type ResolverMessageChoice = { - type: "ResolverMessageChoice"; - payload: SingleAppIntent -} - -/** - * From the app to the intent resolver - */ -export type ResolverIntents = { - type: "ResolverIntents"; - appIntents: AppIntent[], - source: AppIdentifier -} - /** * TODO: Fix this when we have the proper monorepo structure */ export * from './BrowserTypes' -export * from './GetAgent' \ No newline at end of file +export * from './GetAgent' + +export { ChannelSelector } from './ChannelSelector' +export { IntentResolver, IntentResolutionChoice } from './IntentResolver' \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/testing/package.json b/fdc3-for-web-implementation/packages/testing/package.json index 83ea6689b..a1e928d4d 100644 --- a/fdc3-for-web-implementation/packages/testing/package.json +++ b/fdc3-for-web-implementation/packages/testing/package.json @@ -1,6 +1,6 @@ { "name": "@kite9/testing", - "version": "0.0.50", + "version": "0.0.54", "files": [ "dist" ], @@ -16,7 +16,7 @@ "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", "@finos/fdc3": "^2.1.0-beta.6", - "@kite9/fdc3-common": "0.0.50", + "@kite9/fdc3-common": "0.0.54", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", diff --git a/fdc3-for-web-implementation/packages/testing/src/agent/index.ts b/fdc3-for-web-implementation/packages/testing/src/agent/index.ts index 5ed0d6a55..d7f682d4a 100644 --- a/fdc3-for-web-implementation/packages/testing/src/agent/index.ts +++ b/fdc3-for-web-implementation/packages/testing/src/agent/index.ts @@ -1,10 +1,10 @@ -import { AppIntent, IntentResult } from "@finos/fdc3"; -import { IntentResolver, SingleAppIntent } from "@kite9/fdc3-common"; +import { AppIntent, Context, IntentResult } from "@finos/fdc3"; +import { IntentResolver, IntentResolutionChoice } from "@kite9/fdc3-common"; import { PropsWorld } from "../world"; /** * This super-simple intent resolver just resolves to the first - * intent / app in the list. + * intent / app in the list, unless the context is fdc3.cancel-me and then it just cancels. */ export class SimpleIntentResolver implements IntentResolver { @@ -14,19 +14,29 @@ export class SimpleIntentResolver implements IntentResolver { this.cw = cw; } + async init(): Promise { + } + async intentChosen(ir: IntentResult): Promise { this.cw.props['intent-result'] = ir return ir } - async chooseIntent(appIntents: AppIntent[]): Promise { + async chooseIntent(appIntents: AppIntent[], ctx: Context): Promise { + if (ctx.type == 'fdc3.cancel-me') { + return; + } + const out = { intent: appIntents[0].intent, chosenApp: appIntents[0].apps[0] } this.cw.props['intent-resolution'] = out - return out + return { + appId: appIntents[0].apps[0], + intent: appIntents[0].intent.name + } } } From dead99aa05887ac3f441bffa16d7618f67479a4d Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Wed, 28 Aug 2024 15:38:11 +0100 Subject: [PATCH 02/11] Trying to get IR and CS tests working --- fdc3-for-web-implementation/README.md | 2 +- fdc3-for-web-implementation/package-lock.json | 165 ++++++++---------- fdc3-for-web-implementation/package.json | 3 +- .../client/src/ui/AbstractUIComponent.ts | 2 +- .../ui/DefaultDesktopAgentIntentResolver.ts | 2 +- .../features/desktop-agent-strategy.feature | 155 ++++++++-------- .../step-definitions/desktop-agent.steps.ts | 4 +- .../client/test/support/FrameTypes.ts | 65 +++++++ .../client/test/support/MockDocument.ts | 35 ++-- .../client/test/support/MockFDC3Server.ts | 8 +- .../client/test/support/TestServerContext.ts | 22 ++- 11 files changed, 260 insertions(+), 203 deletions(-) create mode 100644 fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts diff --git a/fdc3-for-web-implementation/README.md b/fdc3-for-web-implementation/README.md index 9269291f9..e6507be17 100644 --- a/fdc3-for-web-implementation/README.md +++ b/fdc3-for-web-implementation/README.md @@ -137,7 +137,7 @@ address of the embed page in the cookie? Problem is, the cookie is scoped to the npm login npm version --workspaces npm run syncpack +npm up npm run build npm publish --access=public --workspaces -npm up ``` diff --git a/fdc3-for-web-implementation/package-lock.json b/fdc3-for-web-implementation/package-lock.json index c4f787cbe..5dbf9405b 100644 --- a/fdc3-for-web-implementation/package-lock.json +++ b/fdc3-for-web-implementation/package-lock.json @@ -1,16 +1,15 @@ { "name": "@kite9/web-fdc3", - "version": "0.0.50", + "version": "0.0.54", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@kite9/web-fdc3", - "version": "0.0.50", + "version": "0.0.54", "workspaces": [ "packages/fdc3-common", "packages/testing", - "packages/addon", "packages/da-proxy", "packages/da-server", "packages/client", @@ -1500,8 +1499,8 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.0", - "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==", + "version": "4.21.1", + "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", "cpu": [ "arm" ], @@ -1512,8 +1511,8 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.0", - "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==", + "version": "4.21.1", + "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", "cpu": [ "arm64" ], @@ -1524,8 +1523,8 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.0", - "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==", + "version": "4.21.1", + "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", "cpu": [ "arm64" ], @@ -1536,8 +1535,8 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.0", - "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==", + "version": "4.21.1", + "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", "cpu": [ "x64" ], @@ -1548,8 +1547,8 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.0", - "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==", + "version": "4.21.1", + "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", "cpu": [ "arm" ], @@ -1560,8 +1559,8 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.0", - "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==", + "version": "4.21.1", + "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", "cpu": [ "arm" ], @@ -1572,8 +1571,8 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.0", - "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==", + "version": "4.21.1", + "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", "cpu": [ "arm64" ], @@ -1584,8 +1583,8 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.0", - "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==", + "version": "4.21.1", + "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", "cpu": [ "arm64" ], @@ -1596,8 +1595,8 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.0", - "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==", + "version": "4.21.1", + "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", "cpu": [ "ppc64" ], @@ -1608,8 +1607,8 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.0", - "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==", + "version": "4.21.1", + "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", "cpu": [ "riscv64" ], @@ -1620,8 +1619,8 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.0", - "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==", + "version": "4.21.1", + "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", "cpu": [ "s390x" ], @@ -1643,8 +1642,8 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.0", - "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==", + "version": "4.21.1", + "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", "cpu": [ "x64" ], @@ -1655,8 +1654,8 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.0", - "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==", + "version": "4.21.1", + "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", "cpu": [ "arm64" ], @@ -1667,8 +1666,8 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.0", - "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==", + "version": "4.21.1", + "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", "cpu": [ "ia32" ], @@ -1679,8 +1678,8 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.0", - "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==", + "version": "4.21.1", + "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", "cpu": [ "x64" ], @@ -1867,8 +1866,8 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.16.1", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.2", + "integrity": "sha512-91s/n4qUPV/wg8eE9KHYW1kouTfDk2FPGjXbBMfRWP/2vg1rCXNQL1OCabwGs0XSdukuK+MwCDXE30QpSeMUhQ==", "dependencies": { "undici-types": "~6.19.2" } @@ -2141,10 +2140,6 @@ "node": ">=0.4.0" } }, - "node_modules/addon": { - "resolved": "packages/addon", - "link": true - }, "node_modules/aggregate-error": { "version": "3.1.0", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", @@ -6320,8 +6315,8 @@ } }, "node_modules/ora/node_modules/emoji-regex": { - "version": "10.3.0", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "node_modules/ora/node_modules/string-width": { @@ -7223,8 +7218,8 @@ } }, "node_modules/rollup": { - "version": "4.21.0", - "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==", + "version": "4.21.1", + "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -7237,28 +7232,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.0", - "@rollup/rollup-android-arm64": "4.21.0", - "@rollup/rollup-darwin-arm64": "4.21.0", - "@rollup/rollup-darwin-x64": "4.21.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.0", - "@rollup/rollup-linux-arm-musleabihf": "4.21.0", - "@rollup/rollup-linux-arm64-gnu": "4.21.0", - "@rollup/rollup-linux-arm64-musl": "4.21.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0", - "@rollup/rollup-linux-riscv64-gnu": "4.21.0", - "@rollup/rollup-linux-s390x-gnu": "4.21.0", - "@rollup/rollup-linux-x64-gnu": "4.21.0", - "@rollup/rollup-linux-x64-musl": "4.21.0", - "@rollup/rollup-win32-arm64-msvc": "4.21.0", - "@rollup/rollup-win32-ia32-msvc": "4.21.0", - "@rollup/rollup-win32-x64-msvc": "4.21.0", + "@rollup/rollup-android-arm-eabi": "4.21.1", + "@rollup/rollup-android-arm64": "4.21.1", + "@rollup/rollup-darwin-arm64": "4.21.1", + "@rollup/rollup-darwin-x64": "4.21.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.1", + "@rollup/rollup-linux-arm-musleabihf": "4.21.1", + "@rollup/rollup-linux-arm64-gnu": "4.21.1", + "@rollup/rollup-linux-arm64-musl": "4.21.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", + "@rollup/rollup-linux-riscv64-gnu": "4.21.1", + "@rollup/rollup-linux-s390x-gnu": "4.21.1", + "@rollup/rollup-linux-x64-gnu": "4.21.1", + "@rollup/rollup-linux-x64-musl": "4.21.1", + "@rollup/rollup-win32-arm64-msvc": "4.21.1", + "@rollup/rollup-win32-ia32-msvc": "4.21.1", + "@rollup/rollup-win32-x64-msvc": "4.21.1", "fsevents": "~2.3.2" } }, "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.0", - "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", + "version": "4.21.1", + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", "cpu": [ "x64" ], @@ -8319,8 +8314,8 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/tsx": { - "version": "4.18.0", - "integrity": "sha512-a1jaKBSVQkd6yEc1/NI7G6yHFfefIcuf3QJST7ZEyn4oQnxLYrZR5uZAM8UrwUa3Ge8suiZHcNS1gNrEvmobqg==", + "version": "4.19.0", + "integrity": "sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==", "dependencies": { "esbuild": "~0.23.0", "get-tsconfig": "^4.7.5" @@ -9450,28 +9445,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/addon": { - "version": "0.0.50", - "dependencies": { - "@finos/fdc3": "^2.1.0-beta.6" - }, - "devDependencies": { - "typescript": "^5.3.2", - "vite": "^5.2.0" - } - }, "packages/client": { - "version": "0.0.50", + "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", - "@kite9/da-proxy": "0.0.50", - "@kite9/fdc3-common": "0.0.50", + "@kite9/da-proxy": "0.0.54", + "@kite9/fdc3-common": "0.0.54", "@types/uuid": "^10.0.0", "uuid": "^9.0.1" }, "devDependencies": { "@cucumber/cucumber": "10.3.1", - "@kite9/da-server": "0.0.50", + "@kite9/da-server": "0.0.54", "@types/node": "^20.14.11", "expect": "^29.7.0", "jsonpath-plus": "^9.0.0", @@ -9482,16 +9467,16 @@ } }, "packages/da-proxy": { - "version": "0.0.50", + "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", - "@kite9/fdc3-common": "0.0.50" + "@kite9/fdc3-common": "0.0.54" }, "devDependencies": { "@cucumber/cucumber": "10.3.1", "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", - "@kite9/testing": "0.0.50", + "@kite9/testing": "0.0.54", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", @@ -9516,7 +9501,7 @@ } }, "packages/da-server": { - "version": "0.0.50", + "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", "@types/uuid": "^10.0.0", @@ -9526,8 +9511,8 @@ "@cucumber/cucumber": "10.3.1", "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", - "@kite9/fdc3-common": "0.0.50", - "@kite9/testing": "0.0.50", + "@kite9/fdc3-common": "0.0.54", + "@kite9/testing": "0.0.54", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", @@ -9552,12 +9537,12 @@ } }, "packages/demo": { - "version": "0.0.50", + "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", - "@kite9/client": "0.0.50", - "@kite9/da-server": "0.0.50", - "@kite9/fdc3-common": "0.0.50", + "@kite9/client": "0.0.54", + "@kite9/da-server": "0.0.54", + "@kite9/fdc3-common": "0.0.54", "@types/uuid": "^10.0.0", "@types/ws": "^8.5.10", "express": "^4.18.3", @@ -9577,7 +9562,7 @@ } }, "packages/fdc3-common": { - "version": "0.0.50", + "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6" }, @@ -9587,13 +9572,13 @@ } }, "packages/testing": { - "version": "0.0.50", + "version": "0.0.54", "dependencies": { "@cucumber/cucumber": "10.3.1", "@cucumber/html-formatter": "11.0.4", "@cucumber/pretty-formatter": "1.0.1", "@finos/fdc3": "^2.1.0-beta.6", - "@kite9/fdc3-common": "0.0.50", + "@kite9/fdc3-common": "0.0.54", "@types/expect": "24.3.0", "@types/lodash": "4.14.167", "@types/node": "^20.14.11", diff --git a/fdc3-for-web-implementation/package.json b/fdc3-for-web-implementation/package.json index c731dff43..aa058833c 100644 --- a/fdc3-for-web-implementation/package.json +++ b/fdc3-for-web-implementation/package.json @@ -1,11 +1,10 @@ { "name": "@kite9/web-fdc3", "private": true, - "version": "0.0.50", + "version": "0.0.54", "workspaces": [ "packages/fdc3-common", "packages/testing", - "packages/addon", "packages/da-proxy", "packages/da-server", "packages/client", diff --git a/fdc3-for-web-implementation/packages/client/src/ui/AbstractUIComponent.ts b/fdc3-for-web-implementation/packages/client/src/ui/AbstractUIComponent.ts index c42925b41..b225ebb57 100644 --- a/fdc3-for-web-implementation/packages/client/src/ui/AbstractUIComponent.ts +++ b/fdc3-for-web-implementation/packages/client/src/ui/AbstractUIComponent.ts @@ -86,11 +86,11 @@ export abstract class AbstractUIComponent { this.themeContainer(INITIAL_CONTAINER_CSS) this.themeFrame(ifrm) + this.iframe = ifrm.contentWindow!! ifrm.setAttribute("src", this.url) this.container.appendChild(ifrm) document.body.appendChild(this.container) - this.iframe = ifrm.contentWindow!! } themeContainer(css: CSSPositioning) { diff --git a/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts b/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts index 9333c0c7c..0ef908543 100644 --- a/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts +++ b/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts @@ -1,4 +1,4 @@ -import { AppIdentifier, AppIntent } from "@finos/fdc3"; +import { AppIntent } from "@finos/fdc3"; import { IframeResolveAction, Context, IframeResolve, IntentResolver, IntentResolutionChoice } from "@kite9/fdc3-common"; import { AbstractUIComponent } from "./AbstractUIComponent"; diff --git a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature index 3f1e9519b..0d55fe619 100644 --- a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature +++ b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature @@ -12,82 +12,79 @@ Feature: Different Strategies for Accessing the Desktop Agent | false | 3000 | And I refer to "{result}" as "theAPIPromise" Then the promise "{theAPIPromise}" should resolve - And I call "{result}" with "getInfo" - Then "{result}" is an object with the following contents - | fdc3Version | appMetadata.appId | provider | - | 2.0 | Test App Id | cucumber-provider | - And I refer to "{document.body.children[0]}" as "channel-selector" - And I refer to "{channel-selector.children[0]}" as "iframe" - Then "{iframe}" is an object with the following contents - | tag | atts.name | atts.src | style.width | style.height | - | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | - And "{window.fdc3}" is not null - And "{window.events}" is an array of objects with the following contents - | type | data.type | - | message | WCP1Hello | - | message | WCP3Handshake | - | fdc3Ready | {null} | - Then I call "{document}" with "shutdown" - - Scenario: Running inside a Browser using the embedded iframe strategy - Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response - And we wait for a period of "200" ms - And I call getAgentAPI for a promise result with the following options - | dontSetWindowFdc3 | timeout | - | false | 3000 | - And I refer to "{result}" as "theAPIPromise" - Then the promise "{theAPIPromise}" should resolve - And I call "{result}" with "getInfo" - Then "{result}" is an object with the following contents - | fdc3Version | appMetadata.appId | provider | - | 2.0 | Test App Id | cucumber-provider | - And I refer to "{document.body.children[0]}" as "embedded-iframe" - Then "{embedded-iframe}" is an object with the following contents - | tag | atts.name | style.width | style.height | - | iframe | FDC3 Communications | 0px | 0px | - And I refer to "{document.body.children[1]}" as "channel-selector" - And I refer to "{channel-selector.children[0]}" as "channel-selector-iframe" - Then "{channel-selector-iframe}" is an object with the following contents - | tag | atts.name | atts.src | style.width | style.height | - | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | - And "{window.fdc3}" is not null - And "{window.events}" is an array of objects with the following contents - | type | data.type | - | message | WCP1Hello | - | message | WCP2LoadUrl | - | message | WCP3Handshake | - | fdc3Ready | {null} | - Then I call "{document}" with "shutdown" - - Scenario: Running inside an Electron Container. - In this scenario, window.fdc3 is set by the electron container and returned by getAgentAPI - - Given A Dummy Desktop Agent in "dummy-api" - And I call getAgentAPI for a promise result - And I refer to "{result}" as "theAPIPromise" - And we wait for a period of "500" ms - And `window.fdc3` is injected into the runtime with the value in "{dummy-api}" - Then the promise "{theAPIPromise}" should resolve - And I call "{result}" with "getInfo" - Then "{result}" is an object with the following contents - | fdc3Version | appMetadata.appId | provider | - | 2.0 | cucumber-app | cucumber-provider | - Then I call "{document}" with "shutdown" - - Scenario: Failover Strategy. - Given A Dummy Desktop Agent in "dummy-api" - And "dummyFailover" is a function which returns a promise of "{dummy-api}" - And I call getAgentAPI for a promise result with the following options - | failover | timeout | - | {dummyFailover} | 3000 | - And I refer to "{result}" as "theAPIPromise" - Then the promise "{theAPIPromise}" should resolve - And I call "{result}" with "getInfo" - Then "{result}" is an object with the following contents - | fdc3Version | appMetadata.appId | provider | - | 2.0 | cucumber-app | cucumber-provider | - Then I call "{document}" with "shutdown" - - Scenario: Recovery from SessionState - Here, we recover the details of the session from the session state, obviating the need to - make a request to the parent iframe. + # And I call "{result}" with "getInfo" + # Then "{result}" is an object with the following contents + # | fdc3Version | appMetadata.appId | provider | + # | 2.0 | Test App Id | cucumber-provider | + # And I refer to "{document.body.children[0]}" as "channel-selector" + # And I refer to "{channel-selector.children[0]}" as "iframe" + # Then "{iframe}" is an object with the following contents + # | tag | atts.name | atts.src | style.width | style.height | + # | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | + # And "{window.fdc3}" is not null + # And "{window.events}" is an array of objects with the following contents + # | type | data.type | + # | message | WCP1Hello | + # | message | WCP3Handshake | + # | fdc3Ready | {null} | + # Then I call "{document}" with "shutdown" + # Scenario: Running inside a Browser using the embedded iframe strategy + # Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response + # And we wait for a period of "200" ms + # And I call getAgentAPI for a promise result with the following options + # | dontSetWindowFdc3 | timeout | + # | false | 3000 | + # And I refer to "{result}" as "theAPIPromise" + # Then the promise "{theAPIPromise}" should resolve + # And I call "{result}" with "getInfo" + # Then "{result}" is an object with the following contents + # | fdc3Version | appMetadata.appId | provider | + # | 2.0 | Test App Id | cucumber-provider | + # And I refer to "{document.body.children[0]}" as "embedded-iframe" + # Then "{embedded-iframe}" is an object with the following contents + # | tag | atts.name | style.width | style.height | + # | iframe | FDC3 Communications | 0px | 0px | + # And I refer to "{document.body.children[1]}" as "channel-selector" + # And I refer to "{channel-selector.children[0]}" as "channel-selector-iframe" + # Then "{channel-selector-iframe}" is an object with the following contents + # | tag | atts.name | atts.src | style.width | style.height | + # | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | + # And "{window.fdc3}" is not null + # And "{window.events}" is an array of objects with the following contents + # | type | data.type | + # | message | WCP1Hello | + # | message | WCP2LoadUrl | + # | message | WCP3Handshake | + # | fdc3Ready | {null} | + # Then I call "{document}" with "shutdown" + # Scenario: Running inside an Electron Container. + # In this scenario, window.fdc3 is set by the electron container and returned by getAgentAPI + # Given A Dummy Desktop Agent in "dummy-api" + # And I call getAgentAPI for a promise result + # And I refer to "{result}" as "theAPIPromise" + # And we wait for a period of "500" ms + # And `window.fdc3` is injected into the runtime with the value in "{dummy-api}" + # Then the promise "{theAPIPromise}" should resolve + # And I call "{result}" with "getInfo" + # Then "{result}" is an object with the following contents + # | fdc3Version | appMetadata.appId | provider | + # | 2.0 | cucumber-app | cucumber-provider | + # Then I call "{document}" with "shutdown" + # Scenario: Failover Strategy. + # Given A Dummy Desktop Agent in "dummy-api" + # And "dummyFailover" is a function which returns a promise of "{dummy-api}" + # And I call getAgentAPI for a promise result with the following options + # | failover | timeout | + # | {dummyFailover} | 3000 | + # And I refer to "{result}" as "theAPIPromise" + # Then the promise "{theAPIPromise}" should resolve + # And I call "{result}" with "getInfo" + # Then "{result}" is an object with the following contents + # | fdc3Version | appMetadata.appId | provider | + # | 2.0 | cucumber-app | cucumber-provider | + # Then I call "{document}" with "shutdown" + # Scenario: Recovery from SessionState + # Here, we recover the details of the session from the session state, obviating the need to + # make a request to the parent iframe. + # Scenario: Failed Recovery from SessionState + # App tries to recover with an ID that doesn't exist. diff --git a/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts b/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts index 2a16716a9..23a932e89 100644 --- a/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts +++ b/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts @@ -7,8 +7,8 @@ import { MockDocument, MockWindow } from '../support/MockDocument'; import { getAgent } from '../../src'; import { GetAgentParams } from '@kite9/fdc3-common'; import { dummyInstanceId, MockFDC3Server } from '../support/MockFDC3Server'; -import { DefaultDesktopAgentIntentResolver } from '../../src/intent-resolution/DefaultDesktopAgentIntentResolver'; -import { DefaultDesktopAgentChannelSelector } from '../../src/channel-selector/DefaultDesktopAgentChannelSelector'; +import { DefaultDesktopAgentIntentResolver } from '../../src/ui/DefaultDesktopAgentIntentResolver'; +import { DefaultDesktopAgentChannelSelector } from '../../src/ui/DefaultDesktopAgentChannelSelector'; import { NoopAppSupport } from '../../src/apps/NoopAppSupport'; import { MockStorage } from '../support/MockStorage'; diff --git a/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts b/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts new file mode 100644 index 000000000..5c82e7c09 --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts @@ -0,0 +1,65 @@ +import { IframeHello, WebConnectionProtocol3Handshake } from "@kite9/fdc3-common" +import { CustomWorld } from "../world" +import { MockWindow } from "./MockDocument" +import { CHANNEL_SELECTOR_URL, EMBED_URL, INTENT_RESPOLVER_URL } from "./MockFDC3Server" + + +/** + * This handles the frame communications when we're using the embedded iframe approach + */ +export function handleEmbeddedIframeComms(value: string, parent: MockWindow, cw: CustomWorld) { + const paramStr = value.substring(EMBED_URL.length + 1) + const params = new URLSearchParams(paramStr) + const connectionAttemptUuid = params.get("connectionAttemptUuid")!! + const connection = cw.mockContext.getFirstInstance() + try { + parent.postMessage({ + type: "WCP3Handshake", + meta: { + connectionAttemptUuid: connectionAttemptUuid, + timestamp: new Date() + }, + payload: { + fdc3Version: "2.2", + resolver: INTENT_RESPOLVER_URL, + channelSelector: CHANNEL_SELECTOR_URL, + } + } as WebConnectionProtocol3Handshake, EMBED_URL, [connection!!.externalPort]) + } catch (e) { + console.error(e) + } +} + +export function handleChannelSelectorComms(_value: string, parent: MockWindow, source: Window) { + const connection = new MessageChannel(); + try { + parent.dispatchEvent({ + type: "message", + data: { + type: "iframeHello" + } as IframeHello, + origin: CHANNEL_SELECTOR_URL, + source, + ports: [connection.port1] + } as any as Event) + } catch (e) { + console.error(e) + } +} + +export function handleIntentResolverComms(_value: string, parent: MockWindow, source: Window) { + const connection = new MessageChannel(); + try { + parent.dispatchEvent({ + type: "message", + data: { + type: "iframeHello" + } as IframeHello, + origin: INTENT_RESPOLVER_URL, + source, + ports: [connection.port1] + } as any as Event) + } catch (e) { + console.error(e) + } +} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts b/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts index 90f8e87c8..405a55938 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts @@ -1,7 +1,7 @@ -import { EMBED_URL } from "./MockFDC3Server" +import { CHANNEL_SELECTOR_URL, EMBED_URL, INTENT_RESPOLVER_URL } from "./MockFDC3Server" import { CustomWorld } from "../world" import { DesktopAgent } from "@finos/fdc3" -import { WebConnectionProtocol3Handshake } from "@kite9/fdc3-common" +import { handleChannelSelectorComms, handleEmbeddedIframeComms, handleIntentResolverComms } from "./FrameTypes" class MockCSSStyleDeclaration { @@ -118,9 +118,9 @@ class MockIFrame extends MockWindow { contentWindow: Window - constructor(tag: string, cw: CustomWorld, window: MockWindow) { + constructor(tag: string, cw: CustomWorld, parent: MockWindow) { super(tag, cw) - this.parent = window + this.parent = parent this.contentWindow = this as any } @@ -128,26 +128,13 @@ class MockIFrame extends MockWindow { this.atts[name] = value const parent = this.parent as MockWindow - if ((name == 'src') && (value.startsWith(EMBED_URL))) { - const paramStr = value.substring(EMBED_URL.length + 1) - const params = new URLSearchParams(paramStr) - const connectionAttemptUuid = params.get("connectionAttemptUuid")!! - const connection = this.cw.mockContext.getFirstInstance() - try { - parent.postMessage({ - type: "WCP3Handshake", - meta: { - connectionAttemptUuid: connectionAttemptUuid, - timestamp: new Date() - }, - payload: { - fdc3Version: "2.2", - resolver: "https://mock.fdc3.com/resolver", - channelSelector: "https://mock.fdc3.com/channelSelector", - } - } as WebConnectionProtocol3Handshake, EMBED_URL, [connection!!.externalPort]) - } catch (e) { - console.error(e) + if (name == 'src') { + if (value.startsWith(EMBED_URL)) { + handleEmbeddedIframeComms(value, parent, this.cw) + } else if (value.startsWith(CHANNEL_SELECTOR_URL)) { + handleChannelSelectorComms(value, parent, this.contentWindow) + } else if (value.startsWith(INTENT_RESPOLVER_URL)) { + handleIntentResolverComms(value, parent, this.contentWindow) } } } diff --git a/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts b/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts index 243cbd7d1..ab47a031d 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts @@ -8,6 +8,10 @@ export const dummyInstanceId = { appId: "Test App Id", instanceId: "1" } export const EMBED_URL = "http://localhost:8080/static/da/embed.html" +export const CHANNEL_SELECTOR_URL = "https://mock.fdc3.com/channelSelector" + +export const INTENT_RESPOLVER_URL = "https://mock.fdc3.com/resolver" + export class MockFDC3Server extends DefaultFDC3Server { private useIframe: boolean @@ -68,8 +72,8 @@ export class MockFDC3Server extends DefaultFDC3Server { }, payload: { fdc3Version: "2.2", - resolver: "https://mock.fdc3.com/resolver", - channelSelector: "https://mock.fdc3.com/channelSelector", + resolver: INTENT_RESPOLVER_URL, + channelSelector: CHANNEL_SELECTOR_URL, } } as WebConnectionProtocol3Handshake, origin, [details!!.externalPort]) } diff --git a/fdc3-for-web-implementation/packages/client/test/support/TestServerContext.ts b/fdc3-for-web-implementation/packages/client/test/support/TestServerContext.ts index 0383af00b..dce3bfd86 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/TestServerContext.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/TestServerContext.ts @@ -49,6 +49,23 @@ export class TestServerContext implements ServerContext { this.instances = this.instances.filter(ca => ca.instanceId !== app.instanceId) } + // wrapMessagePort(mp: MessagePort) { + // return { + // postMessage(a: any, b: any) { + // mp.postMessage(a, b) + // } + + // start() { + // mp.start() + // } + + // close() { + // mp.close() + // } + + // } as MessagePortEventMap + // } + async open(appId: string): Promise { const ni = this.nextInstanceId++ if (appId.includes("missing")) { @@ -56,7 +73,10 @@ export class TestServerContext implements ServerContext { } else { const mc = new MessageChannel() const internalPort = mc.port1 - const externalPort = mc.port2 + const externalPort = mc.port2; + + (internalPort as any).name = "internalPort-" + ni; + (externalPort as any).name = "externalPort-" + ni; internalPort.start() From 57a59f763c3d72277ce2c019315f6365b27bacd3 Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Wed, 28 Aug 2024 17:54:02 +0100 Subject: [PATCH 03/11] Allowed timeout to be cancelled when the DA is found --- .../packages/client/src/index.ts | 31 ++++++------- .../src/messaging/MessagePortMessaging.ts | 1 + .../src/strategies/ElectronEventLoader.ts | 41 ++++++++++++++++++ .../packages/client/src/strategies/Loader.ts | 16 +++++++ .../{post-message.ts => PostMessageLoader.ts} | 37 ++++++++++------ .../client/src/strategies/TimeoutLoader.ts | 38 ++++++++++++++++ .../client/src/strategies/electron-event.ts | 31 ------------- .../features/desktop-agent-strategy.feature | 36 ++++++++-------- .../client/test/support/TestMessaging.ts | 6 ++- .../test/support/responses/CurrentChannel.ts | 29 +++++++++++++ .../test/support/responses/UserChannels.ts | 43 +++++++++++++++++++ .../packages/da-server/src/BasicFDC3Server.ts | 2 +- .../packages/fdc3-common/src/index.ts | 4 +- 13 files changed, 233 insertions(+), 82 deletions(-) create mode 100644 fdc3-for-web-implementation/packages/client/src/strategies/ElectronEventLoader.ts create mode 100644 fdc3-for-web-implementation/packages/client/src/strategies/Loader.ts rename fdc3-for-web-implementation/packages/client/src/strategies/{post-message.ts => PostMessageLoader.ts} (74%) create mode 100644 fdc3-for-web-implementation/packages/client/src/strategies/TimeoutLoader.ts delete mode 100644 fdc3-for-web-implementation/packages/client/src/strategies/electron-event.ts create mode 100644 fdc3-for-web-implementation/packages/client/test/support/responses/CurrentChannel.ts create mode 100644 fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts diff --git a/fdc3-for-web-implementation/packages/client/src/index.ts b/fdc3-for-web-implementation/packages/client/src/index.ts index cbd45b2da..212643517 100644 --- a/fdc3-for-web-implementation/packages/client/src/index.ts +++ b/fdc3-for-web-implementation/packages/client/src/index.ts @@ -1,23 +1,13 @@ import { DesktopAgent, } from '@finos/fdc3' import { getAgent as getAgentType, GetAgentParams } from '@kite9/fdc3-common'; -import electronEvent from './strategies/electron-event' -import postMessage from './strategies/post-message' +import electronEvent from './strategies/ElectronEventLoader' +import postMessage from './strategies/PostMessageLoader' +import timeout from './strategies/TimeoutLoader' const DEFAULT_WAIT_FOR_MS = 20000; export const FDC3_VERSION = "2.2" -/** - * Handles getAgent timeout - */ -function timeout(options: GetAgentParams): Promise { - return new Promise((_resolve, reject) => { - setTimeout(() => { - reject(new Error("Timeout waiting for connection")) - }, options.timeout!!) - }) -} - /** * This return an FDC3 API. Should be called by application code. * @@ -53,9 +43,20 @@ export const getAgent: getAgentType = (optionsOverride?: GetAgentParams) => { return da; } - const strategies = STRATEGIES.map(s => s(options)); + const promises = STRATEGIES.map(s => s.get(options)); - return Promise.race(strategies) + return Promise.race(promises) + .then(da => { + // first, cancel the timeout etc. + STRATEGIES.forEach(s => s.cancel()) + + // either the timeout completes first with an error, or one of the other strategies completes with a DesktopAgent. + if (da instanceof Error) { + throw da + } else { + return da as DesktopAgent + } + }) .catch(async (error) => { if (options.failover) { const o = await options.failover(options) diff --git a/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts b/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts index d083677c9..8983422ed 100644 --- a/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts +++ b/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts @@ -24,6 +24,7 @@ export class MessagePortMessaging extends AbstractWebMessaging { this.cd.messagePort.onmessage = (m) => { this.listeners.forEach((v, _k) => { + console.log("Checking", v, m.data) if (v.filter(m.data)) { v.action(m.data) } diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/ElectronEventLoader.ts b/fdc3-for-web-implementation/packages/client/src/strategies/ElectronEventLoader.ts new file mode 100644 index 000000000..ef06c1623 --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/src/strategies/ElectronEventLoader.ts @@ -0,0 +1,41 @@ +import { DesktopAgent } from "@finos/fdc3"; +import { GetAgentParams } from "@kite9/fdc3-common"; +import { Loader } from "./Loader"; + + + +/** + * This approach will resolve the loader promise if the fdc3Ready event occurs. + * This is done by electron implementations setting window.fdc3. + */ +class ElectronEventLoader implements Loader { + + done = false + + poll(endTime: number, resolve: (value: DesktopAgent | Error) => void, reject: (reason?: any) => void) { + const timeRemaining = endTime - Date.now() + if (globalThis.window.fdc3 != null) { + resolve(globalThis.window.fdc3) + } else if ((timeRemaining > 0) && (this.done == false)) { + setTimeout(() => this.poll(endTime, resolve, reject), 100); + } else { + resolve(new Error('timeout')); + } + } + + cancel(): void { + this.done = true; + } + + get(params: GetAgentParams): Promise { + return new Promise((resolve, reject) => { + const endPollTime = Date.now() + (params.timeout + 500) + console.log("Starting poll: " + endPollTime + " " + params.timeout + " " + new Date()) + this.poll(endPollTime, resolve, reject) + }); + } +} + + + +export default new ElectronEventLoader(); diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/Loader.ts b/fdc3-for-web-implementation/packages/client/src/strategies/Loader.ts new file mode 100644 index 000000000..81038a6f8 --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/src/strategies/Loader.ts @@ -0,0 +1,16 @@ +import { DesktopAgent } from "@finos/fdc3" +import { GetAgentParams } from "@kite9/fdc3-common" + +/** + * Represents the common interface for a loading strategy + */ +export interface Loader { + + /** + * Promise will either resolve to a DesktopAgent or _resolve_ to an error (not reject) + */ + get(options: GetAgentParams): Promise + + cancel(): void + +} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/post-message.ts b/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts similarity index 74% rename from fdc3-for-web-implementation/packages/client/src/strategies/post-message.ts rename to fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts index ee90960cb..5455cc382 100644 --- a/fdc3-for-web-implementation/packages/client/src/strategies/post-message.ts +++ b/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts @@ -1,8 +1,10 @@ -import { Loader, GetAgentParams, WebConnectionProtocol1Hello, WebConnectionProtocol2LoadURL } from '@kite9/fdc3-common' +import { GetAgentParams, WebConnectionProtocol1Hello, WebConnectionProtocol2LoadURL } from '@kite9/fdc3-common' import { FDC3_VERSION } from '..'; import { createDesktopAgentAPI } from '../messaging/message-port'; import { v4 as uuidv4 } from "uuid" import { ConnectionDetails } from '../messaging/MessagePortMessaging'; +import { DesktopAgent } from '@finos/fdc3'; +import { Loader } from './Loader'; function collectPossibleTargets(w: Window, found: Window[]) { if (w) { @@ -83,22 +85,31 @@ function helloExchange(options: GetAgentParams, connectionAttemptUuid: string): } -const loader: Loader = async (options: GetAgentParams) => { - const connectionAttemptUuid = uuidv4(); +class PostMessageLoader implements Loader { - const targets: Window[] = [] - collectPossibleTargets(globalThis.window, targets); + connectionAttemptUuid = uuidv4(); - // ok, begin the process - const promise = helloExchange(options, connectionAttemptUuid) + async get(options: GetAgentParams): Promise { + const targets: Window[] = [] + collectPossibleTargets(globalThis.window, targets); - // use of '*': See https://github.com/finos/FDC3/issues/1316 - targets.forEach((t) => sendWCP1Hello(t, options, connectionAttemptUuid, "*")) + // ok, begin the process + const promise = helloExchange(options, this.connectionAttemptUuid) + + // use of '*': See https://github.com/finos/FDC3/issues/1316 + targets.forEach((t) => sendWCP1Hello(t, options, this.connectionAttemptUuid, "*")) + + // wait for one of the windows to return the data we need + const data = await promise + return createDesktopAgentAPI(data); + } + + cancel(): void { + + } - // wait for one of the windows to return the data we need - const data = await promise - return createDesktopAgentAPI(data); } -export default loader; \ No newline at end of file + +export default new PostMessageLoader(); \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/TimeoutLoader.ts b/fdc3-for-web-implementation/packages/client/src/strategies/TimeoutLoader.ts new file mode 100644 index 000000000..b371d4de2 --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/src/strategies/TimeoutLoader.ts @@ -0,0 +1,38 @@ +import { DesktopAgent } from "@finos/fdc3"; +import { GetAgentParams } from "@kite9/fdc3-common"; +import { Loader } from "./Loader"; + + + +/** + * This loader handles timing out. + */ +class TimeoutLoader implements Loader { + + done = false + + poll(endTime: number, resolve: (value: DesktopAgent | Error) => void, reject: (reason?: any) => void) { + const timeRemaining = endTime - Date.now() + + if ((timeRemaining > 0) && (this.done == false)) { + setTimeout(() => this.poll(endTime, resolve, reject), 100); + } else { + reject(new Error('timeout')); + } + } + + cancel(): void { + this.done = true; + } + + get(params: GetAgentParams): Promise { + return new Promise((resolve, reject) => { + const endPollTime = Date.now() + params.timeout + this.poll(endPollTime, resolve, reject) + }); + } +} + + + +export default new TimeoutLoader(); diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/electron-event.ts b/fdc3-for-web-implementation/packages/client/src/strategies/electron-event.ts deleted file mode 100644 index 33c6f8e4c..000000000 --- a/fdc3-for-web-implementation/packages/client/src/strategies/electron-event.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DesktopAgent } from "@finos/fdc3"; -import { Loader, GetAgentParams } from "@kite9/fdc3-common"; - - -function poll(endTime: number, resolve: (value: DesktopAgent | PromiseLike) => void, reject: (reason?: any) => void) { - const timeRemaining = endTime - Date.now() - if (globalThis.window.fdc3 != null) { - resolve(globalThis.window.fdc3) - } else if (timeRemaining > 0) { - setTimeout(() => poll(endTime, resolve, reject), 100); - } else { - reject(new Error('timeout')); - } -} - -/** - * This approach will resolve the loader promise if the fdc3Ready event occurs. - * This is done by electron implementations setting window.fdc3. - */ -const loader: Loader = (options: GetAgentParams) => { - - const out = new Promise((resolve, reject) => { - const endPollTime = Date.now() + options.timeout!! - console.log("Starting poll: " + endPollTime + " " + options.timeout + " " + new Date()) - poll(endPollTime, resolve, reject) - }); - - return out; -} - -export default loader; diff --git a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature index 0d55fe619..030d51157 100644 --- a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature +++ b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature @@ -2,38 +2,38 @@ Feature: Different Strategies for Accessing the Desktop Agent Background: Desktop Agent API Given a browser document in "document" and window in "window" - And Testing ends after "5000" ms + And Testing ends after "8000" ms Scenario: Running inside a Browser and using post message with direct message ports Given Parent Window desktop "da" listens for postMessage events in "{window}", returns direct message response And we wait for a period of "200" ms And I call getAgentAPI for a promise result with the following options - | dontSetWindowFdc3 | timeout | - | false | 3000 | + | dontSetWindowFdc3 | timeout | intentResolver | channelSelector | + | false | 8000 | false | false | And I refer to "{result}" as "theAPIPromise" Then the promise "{theAPIPromise}" should resolve - # And I call "{result}" with "getInfo" - # Then "{result}" is an object with the following contents - # | fdc3Version | appMetadata.appId | provider | - # | 2.0 | Test App Id | cucumber-provider | - # And I refer to "{document.body.children[0]}" as "channel-selector" - # And I refer to "{channel-selector.children[0]}" as "iframe" + And I call "{result}" with "getInfo" + Then "{result}" is an object with the following contents + | fdc3Version | appMetadata.appId | provider | + | 2.0 | Test App Id | cucumber-provider | + And I refer to "{document.body.children[0]}" as "channel-selector" + And I refer to "{channel-selector.children[0]}" as "iframe" # Then "{iframe}" is an object with the following contents # | tag | atts.name | atts.src | style.width | style.height | # | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | - # And "{window.fdc3}" is not null - # And "{window.events}" is an array of objects with the following contents - # | type | data.type | - # | message | WCP1Hello | - # | message | WCP3Handshake | - # | fdc3Ready | {null} | - # Then I call "{document}" with "shutdown" + And "{window.fdc3}" is not null + And "{window.events}" is an array of objects with the following contents + | type | data.type | + | message | WCP1Hello | + | message | WCP3Handshake | + | fdc3Ready | {null} | + Then I call "{document}" with "shutdown" # Scenario: Running inside a Browser using the embedded iframe strategy # Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response # And we wait for a period of "200" ms # And I call getAgentAPI for a promise result with the following options # | dontSetWindowFdc3 | timeout | - # | false | 3000 | + # | false | 8000 | # And I refer to "{result}" as "theAPIPromise" # Then the promise "{theAPIPromise}" should resolve # And I call "{result}" with "getInfo" @@ -75,7 +75,7 @@ Feature: Different Strategies for Accessing the Desktop Agent # And "dummyFailover" is a function which returns a promise of "{dummy-api}" # And I call getAgentAPI for a promise result with the following options # | failover | timeout | - # | {dummyFailover} | 3000 | + # | {dummyFailover} | 8000 | # And I refer to "{result}" as "theAPIPromise" # Then the promise "{theAPIPromise}" should resolve # And I call "{result}" with "getInfo" diff --git a/fdc3-for-web-implementation/packages/client/test/support/TestMessaging.ts b/fdc3-for-web-implementation/packages/client/test/support/TestMessaging.ts index bf6dc4223..51e65d93e 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/TestMessaging.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/TestMessaging.ts @@ -3,9 +3,11 @@ import { RegisterableListener } from "@kite9/da-proxy"; import { AppRequestMessage } from "@kite9/fdc3-common"; import { v4 as uuidv4 } from 'uuid' import { AbstractWebMessaging } from "../../src/messaging/AbstractWebMessaging"; +import { CurrentChannel } from "./responses/CurrentChannel"; import { FindIntent } from "./responses/FindIntent"; import { Handshake } from "./responses/Handshake"; import { RaiseIntent } from "./responses/RaiseIntent"; +import { UserChannels } from "./responses/UserChannels"; export interface AutomaticResponse { @@ -32,7 +34,9 @@ export class TestMessaging extends AbstractWebMessaging { readonly automaticResponses: AutomaticResponse[] = [ new FindIntent(), new RaiseIntent(), - new Handshake() + new Handshake(), + new UserChannels(), + new CurrentChannel() ] register(l: RegisterableListener) { diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/CurrentChannel.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/CurrentChannel.ts new file mode 100644 index 000000000..556dfb6ea --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/CurrentChannel.ts @@ -0,0 +1,29 @@ +import { AutomaticResponse, TestMessaging } from "../TestMessaging"; +import { GetCurrentChannelRequest, GetCurrentChannelResponse } from "@kite9/fdc3-common"; + +export class CurrentChannel implements AutomaticResponse { + + filter(t: string) { + return t == 'getCurrentChannelRequest' + } + + action(input: object, m: TestMessaging) { + const out = this.createResponse(input as GetCurrentChannelRequest, m) + + setTimeout(() => { m.receive(out) }, 100) + return Promise.resolve() + } + + private createResponse(i: GetCurrentChannelRequest, m: TestMessaging): GetCurrentChannelResponse { + return { + meta: { + ...i.meta, + responseUuid: m.createUUID(), + }, + type: "getCurrentChannelResponse", + payload: { + + } + } + } +} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts new file mode 100644 index 000000000..d0c4052e8 --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts @@ -0,0 +1,43 @@ +import { AutomaticResponse, TestMessaging } from "../TestMessaging"; +import { GetUserChannelsRequest, GetUserChannelsResponse } from "@kite9/fdc3-common"; + +export class UserChannels implements AutomaticResponse { + + filter(t: string) { + return t == 'getUserChannelsRequest' + } + + action(input: object, m: TestMessaging) { + const out = this.createResponse(input as GetUserChannelsRequest, m) + + setTimeout(() => { m.receive(out) }, 100) + return Promise.resolve() + } + + private createResponse(i: GetUserChannelsRequest, m: TestMessaging): GetUserChannelsResponse { + return { + meta: { + ...i.meta, + responseUuid: m.createUUID(), + }, + type: "getUserChannelsResponse", + payload: { + userChannels: [ + { + id: "one", + type: "user" + }, + { + id: "two", + type: "user" + }, + { + id: "three", + type: "user" + } + ] + + } + } + } +} \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/da-server/src/BasicFDC3Server.ts b/fdc3-for-web-implementation/packages/da-server/src/BasicFDC3Server.ts index c5cb69862..7d49d2c81 100644 --- a/fdc3-for-web-implementation/packages/da-server/src/BasicFDC3Server.ts +++ b/fdc3-for-web-implementation/packages/da-server/src/BasicFDC3Server.ts @@ -29,7 +29,7 @@ export class BasicFDC3Server implements FDC3Server { } receive(message: AppRequestMessage | WebConnectionProtocol4ValidateAppIdentity, from: InstanceID): void { - this.sc.log(`MessageReceived: \n ${JSON.stringify(message, null, 2)}`) + // this.sc.log(`MessageReceived: \n ${JSON.stringify(message, null, 2)}`) this.handlers.forEach(h => h.accept(message, this.sc, from)) } } diff --git a/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts b/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts index f50c813c9..0971d32b1 100644 --- a/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts +++ b/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts @@ -1,11 +1,9 @@ import { - AppIdentifier, DesktopAgent + AppIdentifier } from "@finos/fdc3"; -import { GetAgentParams } from "./GetAgent"; export type AppChecker = (o: Window) => AppIdentifier | undefined; -export type Loader = (options: GetAgentParams) => Promise /** * TODO: Fix this when we have the proper monorepo structure From 8e736bcc93d5fd029cc05bfaa6bd4221c1e749fd Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Wed, 28 Aug 2024 18:13:36 +0100 Subject: [PATCH 04/11] First two tests working --- .../packages/client/src/index.ts | 18 ++--- .../src/strategies/ElectronEventLoader.ts | 15 ++-- .../packages/client/src/strategies/Loader.ts | 2 +- .../src/strategies/PostMessageLoader.ts | 7 +- .../client/src/strategies/TimeoutLoader.ts | 16 ++--- .../features/desktop-agent-strategy.feature | 68 ++++++++++--------- 6 files changed, 60 insertions(+), 66 deletions(-) diff --git a/fdc3-for-web-implementation/packages/client/src/index.ts b/fdc3-for-web-implementation/packages/client/src/index.ts index 212643517..588d33203 100644 --- a/fdc3-for-web-implementation/packages/client/src/index.ts +++ b/fdc3-for-web-implementation/packages/client/src/index.ts @@ -1,8 +1,8 @@ import { DesktopAgent, } from '@finos/fdc3' import { getAgent as getAgentType, GetAgentParams } from '@kite9/fdc3-common'; -import electronEvent from './strategies/ElectronEventLoader' -import postMessage from './strategies/PostMessageLoader' -import timeout from './strategies/TimeoutLoader' +import { ElectronEventLoader } from './strategies/ElectronEventLoader' +import { PostMessageLoader } from './strategies/PostMessageLoader' +import { TimeoutLoader } from './strategies/TimeoutLoader' const DEFAULT_WAIT_FOR_MS = 20000; @@ -29,9 +29,9 @@ export const getAgent: getAgentType = (optionsOverride?: GetAgentParams) => { } const STRATEGIES = [ - electronEvent, - postMessage, - timeout + new ElectronEventLoader(), + new PostMessageLoader(), + new TimeoutLoader() ] function handleGenericOptions(da: DesktopAgent) { @@ -51,10 +51,10 @@ export const getAgent: getAgentType = (optionsOverride?: GetAgentParams) => { STRATEGIES.forEach(s => s.cancel()) // either the timeout completes first with an error, or one of the other strategies completes with a DesktopAgent. - if (da instanceof Error) { - throw da - } else { + if (da) { return da as DesktopAgent + } else { + throw new Error("No DesktopAgent found") } }) .catch(async (error) => { diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/ElectronEventLoader.ts b/fdc3-for-web-implementation/packages/client/src/strategies/ElectronEventLoader.ts index ef06c1623..aa61695b1 100644 --- a/fdc3-for-web-implementation/packages/client/src/strategies/ElectronEventLoader.ts +++ b/fdc3-for-web-implementation/packages/client/src/strategies/ElectronEventLoader.ts @@ -8,18 +8,18 @@ import { Loader } from "./Loader"; * This approach will resolve the loader promise if the fdc3Ready event occurs. * This is done by electron implementations setting window.fdc3. */ -class ElectronEventLoader implements Loader { +export class ElectronEventLoader implements Loader { done = false - poll(endTime: number, resolve: (value: DesktopAgent | Error) => void, reject: (reason?: any) => void) { + poll(endTime: number, resolve: (value: DesktopAgent | void) => void, reject: (reason?: any) => void) { const timeRemaining = endTime - Date.now() if (globalThis.window.fdc3 != null) { resolve(globalThis.window.fdc3) } else if ((timeRemaining > 0) && (this.done == false)) { setTimeout(() => this.poll(endTime, resolve, reject), 100); } else { - resolve(new Error('timeout')); + resolve(); } } @@ -27,15 +27,12 @@ class ElectronEventLoader implements Loader { this.done = true; } - get(params: GetAgentParams): Promise { - return new Promise((resolve, reject) => { + get(params: GetAgentParams): Promise { + return new Promise((resolve, reject) => { const endPollTime = Date.now() + (params.timeout + 500) - console.log("Starting poll: " + endPollTime + " " + params.timeout + " " + new Date()) + console.log("Starting poll: " + endPollTime + " " + params.timeout + " " + Date.now()) this.poll(endPollTime, resolve, reject) }); } } - - -export default new ElectronEventLoader(); diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/Loader.ts b/fdc3-for-web-implementation/packages/client/src/strategies/Loader.ts index 81038a6f8..dbc2579b7 100644 --- a/fdc3-for-web-implementation/packages/client/src/strategies/Loader.ts +++ b/fdc3-for-web-implementation/packages/client/src/strategies/Loader.ts @@ -9,7 +9,7 @@ export interface Loader { /** * Promise will either resolve to a DesktopAgent or _resolve_ to an error (not reject) */ - get(options: GetAgentParams): Promise + get(options: GetAgentParams): Promise cancel(): void diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts b/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts index 5455cc382..791152e00 100644 --- a/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts +++ b/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts @@ -85,11 +85,11 @@ function helloExchange(options: GetAgentParams, connectionAttemptUuid: string): } -class PostMessageLoader implements Loader { +export class PostMessageLoader implements Loader { connectionAttemptUuid = uuidv4(); - async get(options: GetAgentParams): Promise { + async get(options: GetAgentParams): Promise { const targets: Window[] = [] collectPossibleTargets(globalThis.window, targets); @@ -110,6 +110,3 @@ class PostMessageLoader implements Loader { } - - -export default new PostMessageLoader(); \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/TimeoutLoader.ts b/fdc3-for-web-implementation/packages/client/src/strategies/TimeoutLoader.ts index b371d4de2..9ad01a225 100644 --- a/fdc3-for-web-implementation/packages/client/src/strategies/TimeoutLoader.ts +++ b/fdc3-for-web-implementation/packages/client/src/strategies/TimeoutLoader.ts @@ -7,17 +7,19 @@ import { Loader } from "./Loader"; /** * This loader handles timing out. */ -class TimeoutLoader implements Loader { +export class TimeoutLoader implements Loader { done = false - poll(endTime: number, resolve: (value: DesktopAgent | Error) => void, reject: (reason?: any) => void) { + poll(endTime: number, resolve: (value: DesktopAgent | void) => void, reject: (reason?: any) => void) { const timeRemaining = endTime - Date.now() if ((timeRemaining > 0) && (this.done == false)) { setTimeout(() => this.poll(endTime, resolve, reject), 100); - } else { + } else if (this.done == false) { reject(new Error('timeout')); + } else { + resolve(); } } @@ -25,14 +27,10 @@ class TimeoutLoader implements Loader { this.done = true; } - get(params: GetAgentParams): Promise { - return new Promise((resolve, reject) => { + get(params: GetAgentParams): Promise { + return new Promise((resolve, reject) => { const endPollTime = Date.now() + params.timeout this.poll(endPollTime, resolve, reject) }); } } - - - -export default new TimeoutLoader(); diff --git a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature index 030d51157..012d96aab 100644 --- a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature +++ b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature @@ -9,7 +9,7 @@ Feature: Different Strategies for Accessing the Desktop Agent And we wait for a period of "200" ms And I call getAgentAPI for a promise result with the following options | dontSetWindowFdc3 | timeout | intentResolver | channelSelector | - | false | 8000 | false | false | + | true | 8000 | false | false | And I refer to "{result}" as "theAPIPromise" Then the promise "{theAPIPromise}" should resolve And I call "{result}" with "getInfo" @@ -18,45 +18,47 @@ Feature: Different Strategies for Accessing the Desktop Agent | 2.0 | Test App Id | cucumber-provider | And I refer to "{document.body.children[0]}" as "channel-selector" And I refer to "{channel-selector.children[0]}" as "iframe" - # Then "{iframe}" is an object with the following contents - # | tag | atts.name | atts.src | style.width | style.height | - # | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | + And "{window.fdc3}" is undefined + And "{window.events}" is an array of objects with the following contents + | type | data.type | + | message | WCP1Hello | + | message | WCP3Handshake | + Then I call "{document}" with "shutdown" + + Scenario: Running inside a Browser using the embedded iframe strategy + Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response + And we wait for a period of "200" ms + And I call getAgentAPI for a promise result with the following options + | dontSetWindowFdc3 | timeout | + | false | 8000 | + And I refer to "{result}" as "theAPIPromise" + Then the promise "{theAPIPromise}" should resolve + And I call "{result}" with "getInfo" + Then "{result}" is an object with the following contents + | fdc3Version | appMetadata.appId | provider | + | 2.0 | Test App Id | cucumber-provider | + And I refer to "{document.iframes[0]}" as "embedded-iframe" + Then "{embedded-iframe}" is an object with the following contents + | tag | atts.name | style.width | style.height | + | iframe | FDC3 Communications | 0px | 0px | + And I refer to "{document.iframes[1]}" as "intent-resolver-iframe" + And I refer to "{document.iframes[2]}" as "channel-selector-iframe" + Then "{channel-selector-iframe}" is an object with the following contents + | tag | atts.name | atts.src | style.width | style.height | + | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | + Then "{intent-resolver-iframe}" is an object with the following contents + | tag | atts.name | atts.src | style.width | style.height | + | iframe | FDC3 Intent Resolver | https://mock.fdc3.com/resolver | 100% | 100% | And "{window.fdc3}" is not null And "{window.events}" is an array of objects with the following contents | type | data.type | | message | WCP1Hello | + | message | WCP2LoadUrl | | message | WCP3Handshake | + | message | iframeHello | + | message | iframeHello | | fdc3Ready | {null} | Then I call "{document}" with "shutdown" - # Scenario: Running inside a Browser using the embedded iframe strategy - # Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response - # And we wait for a period of "200" ms - # And I call getAgentAPI for a promise result with the following options - # | dontSetWindowFdc3 | timeout | - # | false | 8000 | - # And I refer to "{result}" as "theAPIPromise" - # Then the promise "{theAPIPromise}" should resolve - # And I call "{result}" with "getInfo" - # Then "{result}" is an object with the following contents - # | fdc3Version | appMetadata.appId | provider | - # | 2.0 | Test App Id | cucumber-provider | - # And I refer to "{document.body.children[0]}" as "embedded-iframe" - # Then "{embedded-iframe}" is an object with the following contents - # | tag | atts.name | style.width | style.height | - # | iframe | FDC3 Communications | 0px | 0px | - # And I refer to "{document.body.children[1]}" as "channel-selector" - # And I refer to "{channel-selector.children[0]}" as "channel-selector-iframe" - # Then "{channel-selector-iframe}" is an object with the following contents - # | tag | atts.name | atts.src | style.width | style.height | - # | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | - # And "{window.fdc3}" is not null - # And "{window.events}" is an array of objects with the following contents - # | type | data.type | - # | message | WCP1Hello | - # | message | WCP2LoadUrl | - # | message | WCP3Handshake | - # | fdc3Ready | {null} | - # Then I call "{document}" with "shutdown" # Scenario: Running inside an Electron Container. # In this scenario, window.fdc3 is set by the electron container and returned by getAgentAPI # Given A Dummy Desktop Agent in "dummy-api" From 2295af490ab5fb5e93d0c79fc0a63a9a7ecf4184 Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Thu, 29 Aug 2024 09:27:16 +0100 Subject: [PATCH 05/11] Client tests shutting down gracefully --- .../packages/client/package.json | 4 +- .../src/messaging/MessagePortMessaging.ts | 1 - .../client/src/messaging/message-port.ts | 5 +- .../client/src/ui/AbstractUIComponent.ts | 18 +++-- .../ui/DefaultDesktopAgentChannelSelector.ts | 1 - .../ui/DefaultDesktopAgentIntentResolver.ts | 1 - .../client/src/ui/NullChannelSelector.ts | 8 +- .../client/src/ui/NullIIntentResolver.ts | 5 +- .../features/desktop-agent-strategy.feature | 77 ++++++++++--------- .../step-definitions/desktop-agent.steps.ts | 11 ++- .../client/test/support/FrameTypes.ts | 8 +- .../client/test/support/MockDocument.ts | 13 +++- .../client/test/support/MockFDC3Server.ts | 5 +- .../client/test/support/TestServerContext.ts | 20 +---- .../da-proxy/src/BasicDesktopAgent.ts | 14 ++-- .../packages/da-proxy/src/Messaging.ts | 2 +- .../src/channels/DefaultChannelSupport.ts | 7 +- .../src/handshake/HandshakeSupport.ts | 2 +- .../test/step-definitions/generic.steps.ts | 2 +- .../fdc3-common/src/ChannelSelector.ts | 8 +- .../src/Connectable.ts | 0 .../fdc3-common/src/IntentResolver.ts | 8 +- .../packages/fdc3-common/src/index.ts | 3 +- .../packages/testing/src/agent/index.ts | 5 +- 24 files changed, 125 insertions(+), 103 deletions(-) rename fdc3-for-web-implementation/packages/{da-proxy => fdc3-common}/src/Connectable.ts (100%) diff --git a/fdc3-for-web-implementation/packages/client/package.json b/fdc3-for-web-implementation/packages/client/package.json index 8fd084faa..bd69e467f 100644 --- a/fdc3-for-web-implementation/packages/client/package.json +++ b/fdc3-for-web-implementation/packages/client/package.json @@ -22,11 +22,13 @@ "@cucumber/cucumber": "10.3.1", "@kite9/da-server": "0.0.54", "@types/node": "^20.14.11", + "@types/wtfnode": "^0.7.3", "expect": "^29.7.0", "jsonpath-plus": "^9.0.0", "nyc": "15.1.0", "rimraf": "^6.0.1", "tsx": "^4.7.1", - "typescript": "^5.3.2" + "typescript": "^5.3.2", + "wtfnode": "^0.9.3" } } diff --git a/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts b/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts index 8983422ed..316e70efe 100644 --- a/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts +++ b/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts @@ -56,6 +56,5 @@ export class MessagePortMessaging extends AbstractWebMessaging { "source": this.getSource() } } - } diff --git a/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts b/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts index cbc220a73..b9dae6c04 100644 --- a/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts +++ b/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts @@ -35,14 +35,11 @@ export async function createDesktopAgentAPI(cd: ConnectionDetails): Promise void) | null = null - private port: MessagePort | null = null constructor(url: string | null) { super(url ?? "https://fdc3.finos.org/webui/channel_selector.html", "FDC3 Channel Selector") diff --git a/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts b/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts index 0ef908543..005ad5035 100644 --- a/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts +++ b/fdc3-for-web-implementation/packages/client/src/ui/DefaultDesktopAgentIntentResolver.ts @@ -9,7 +9,6 @@ import { AbstractUIComponent } from "./AbstractUIComponent"; */ export class DefaultDesktopAgentIntentResolver extends AbstractUIComponent implements IntentResolver { - private port: MessagePort | null = null private pendingResolve: ((x: IntentResolutionChoice | void) => void) | null = null constructor(url: string | null) { diff --git a/fdc3-for-web-implementation/packages/client/src/ui/NullChannelSelector.ts b/fdc3-for-web-implementation/packages/client/src/ui/NullChannelSelector.ts index e1a4a2ab6..f70904030 100644 --- a/fdc3-for-web-implementation/packages/client/src/ui/NullChannelSelector.ts +++ b/fdc3-for-web-implementation/packages/client/src/ui/NullChannelSelector.ts @@ -1,9 +1,13 @@ import { Channel, } from "@finos/fdc3"; +import { Connectable } from "@kite9/fdc3-common"; import { ChannelSelector } from "@kite9/fdc3-common"; -export class NullChannelSelector implements ChannelSelector { +export class NullChannelSelector implements ChannelSelector, Connectable { - async init(): Promise { + async disconnect(): Promise { + } + + async connect(): Promise { } updateChannel(_channelId: string | null, _availableChannels: Channel[]): void { diff --git a/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts b/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts index dd68a0381..dc989f32c 100644 --- a/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts +++ b/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts @@ -5,7 +5,10 @@ import { IntentResolutionChoice } from "@kite9/fdc3-common/src/IntentResolver"; export class NullIntentResolver implements IntentResolver { - async init(): Promise { + async disconnect(): Promise { + } + + async connect(): Promise { } chooseIntent(_appIntents: AppIntent[], _ctx: Context): Promise { diff --git a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature index 012d96aab..91e939068 100644 --- a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature +++ b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature @@ -2,7 +2,7 @@ Feature: Different Strategies for Accessing the Desktop Agent Background: Desktop Agent API Given a browser document in "document" and window in "window" - And Testing ends after "8000" ms + # And Testing ends after "8000" ms Scenario: Running inside a Browser and using post message with direct message ports Given Parent Window desktop "da" listens for postMessage events in "{window}", returns direct message response @@ -12,7 +12,8 @@ Feature: Different Strategies for Accessing the Desktop Agent | true | 8000 | false | false | And I refer to "{result}" as "theAPIPromise" Then the promise "{theAPIPromise}" should resolve - And I call "{result}" with "getInfo" + And I refer to "{result}" as "desktopAgent" + And I call "{desktopAgent}" with "getInfo" Then "{result}" is an object with the following contents | fdc3Version | appMetadata.appId | provider | | 2.0 | Test App Id | cucumber-provider | @@ -24,41 +25,43 @@ Feature: Different Strategies for Accessing the Desktop Agent | message | WCP1Hello | | message | WCP3Handshake | Then I call "{document}" with "shutdown" - - Scenario: Running inside a Browser using the embedded iframe strategy - Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response - And we wait for a period of "200" ms - And I call getAgentAPI for a promise result with the following options - | dontSetWindowFdc3 | timeout | - | false | 8000 | - And I refer to "{result}" as "theAPIPromise" - Then the promise "{theAPIPromise}" should resolve - And I call "{result}" with "getInfo" - Then "{result}" is an object with the following contents - | fdc3Version | appMetadata.appId | provider | - | 2.0 | Test App Id | cucumber-provider | - And I refer to "{document.iframes[0]}" as "embedded-iframe" - Then "{embedded-iframe}" is an object with the following contents - | tag | atts.name | style.width | style.height | - | iframe | FDC3 Communications | 0px | 0px | - And I refer to "{document.iframes[1]}" as "intent-resolver-iframe" - And I refer to "{document.iframes[2]}" as "channel-selector-iframe" - Then "{channel-selector-iframe}" is an object with the following contents - | tag | atts.name | atts.src | style.width | style.height | - | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | - Then "{intent-resolver-iframe}" is an object with the following contents - | tag | atts.name | atts.src | style.width | style.height | - | iframe | FDC3 Intent Resolver | https://mock.fdc3.com/resolver | 100% | 100% | - And "{window.fdc3}" is not null - And "{window.events}" is an array of objects with the following contents - | type | data.type | - | message | WCP1Hello | - | message | WCP2LoadUrl | - | message | WCP3Handshake | - | message | iframeHello | - | message | iframeHello | - | fdc3Ready | {null} | - Then I call "{document}" with "shutdown" + And I call "{desktopAgent}" with "disconnect" + # Scenario: Running inside a Browser using the embedded iframe strategy + # Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response + # And we wait for a period of "200" ms + # And I call getAgentAPI for a promise result with the following options + # | dontSetWindowFdc3 | timeout | + # | false | 8000 | + # And I refer to "{result}" as "theAPIPromise" + # Then the promise "{theAPIPromise}" should resolve + # And I refer to "{result}" as "desktopAgent" + # And I call "{desktopAgent}" with "getInfo" + # Then "{result}" is an object with the following contents + # | fdc3Version | appMetadata.appId | provider | + # | 2.0 | Test App Id | cucumber-provider | + # And I refer to "{document.iframes[0]}" as "embedded-iframe" + # Then "{embedded-iframe}" is an object with the following contents + # | tag | atts.name | style.width | style.height | + # | iframe | FDC3 Communications | 0px | 0px | + # And I refer to "{document.iframes[1]}" as "intent-resolver-iframe" + # And I refer to "{document.iframes[2]}" as "channel-selector-iframe" + # Then "{channel-selector-iframe}" is an object with the following contents + # | tag | atts.name | atts.src | style.width | style.height | + # | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | + # Then "{intent-resolver-iframe}" is an object with the following contents + # | tag | atts.name | atts.src | style.width | style.height | + # | iframe | FDC3 Intent Resolver | https://mock.fdc3.com/resolver | 100% | 100% | + # And "{window.fdc3}" is not null + # And "{window.events}" is an array of objects with the following contents + # | type | data.type | + # | message | WCP1Hello | + # | message | WCP2LoadUrl | + # | message | WCP3Handshake | + # | message | iframeHello | + # | message | iframeHello | + # | fdc3Ready | {null} | + # Then I call "{document}" with "shutdown" + # And I call "{desktopAgent}" with "disconnect" # Scenario: Running inside an Electron Container. # In this scenario, window.fdc3 is set by the electron container and returned by getAgentAPI # Given A Dummy Desktop Agent in "dummy-api" diff --git a/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts b/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts index 23a932e89..2ef0e09ac 100644 --- a/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts +++ b/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts @@ -1,4 +1,4 @@ -import { DataTable, Given, When } from '@cucumber/cucumber' +import { After, DataTable, Given, When } from '@cucumber/cucumber' import { CustomWorld } from '../world'; import { TestMessaging } from '../support/TestMessaging'; import { handleResolve, setupGenericSteps } from '@kite9/testing'; @@ -11,6 +11,7 @@ import { DefaultDesktopAgentIntentResolver } from '../../src/ui/DefaultDesktopAg import { DefaultDesktopAgentChannelSelector } from '../../src/ui/DefaultDesktopAgentChannelSelector'; import { NoopAppSupport } from '../../src/apps/NoopAppSupport'; import { MockStorage } from '../support/MockStorage'; +var wtf = require('wtfnode') setupGenericSteps() Given('Parent Window desktop {string} listens for postMessage events in {string}, returns direct message response', async function (this: CustomWorld, field: string, w: string) { @@ -41,7 +42,7 @@ Given('A Dummy Desktop Agent in {string}', async function (this: CustomWorld, fi const is = new DefaultIntentSupport(this.messaging, intentResolver) const as = new NoopAppSupport(this.messaging) - const da = new BasicDesktopAgent(hs, cs, is, as) + const da = new BasicDesktopAgent(hs, cs, is, as, [hs, intentResolver, channelSelector]) await da.connect() this.props[field] = da @@ -62,6 +63,12 @@ When('I call getAgentAPI for a promise result', function (this: CustomWorld) { } }) +After(function (this: CustomWorld) { + console.log("Cleaning up") + console.log((process as any)._getActiveHandles()) + setTimeout(() => { wtf.dump() }, 10000) +}) + When('I call getAgentAPI for a promise result with the following options', function (this: CustomWorld, dt: DataTable) { try { const first = dt.hashes()[0] diff --git a/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts b/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts index 5c82e7c09..fa5219856 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts @@ -30,7 +30,7 @@ export function handleEmbeddedIframeComms(value: string, parent: MockWindow, cw: } } -export function handleChannelSelectorComms(_value: string, parent: MockWindow, source: Window) { +export function handleChannelSelectorComms(_value: string, parent: MockWindow, source: Window): MessageChannel { const connection = new MessageChannel(); try { parent.dispatchEvent({ @@ -45,9 +45,11 @@ export function handleChannelSelectorComms(_value: string, parent: MockWindow, s } catch (e) { console.error(e) } + + return connection } -export function handleIntentResolverComms(_value: string, parent: MockWindow, source: Window) { +export function handleIntentResolverComms(_value: string, parent: MockWindow, source: Window): MessageChannel { const connection = new MessageChannel(); try { parent.dispatchEvent({ @@ -62,4 +64,6 @@ export function handleIntentResolverComms(_value: string, parent: MockWindow, so } catch (e) { console.error(e) } + + return connection } \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts b/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts index 405a55938..6662a5814 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts @@ -117,6 +117,7 @@ export class MockWindow extends MockElement { class MockIFrame extends MockWindow { contentWindow: Window + messageChannels: MessageChannel[] = [] constructor(tag: string, cw: CustomWorld, parent: MockWindow) { super(tag, cw) @@ -132,12 +133,20 @@ class MockIFrame extends MockWindow { if (value.startsWith(EMBED_URL)) { handleEmbeddedIframeComms(value, parent, this.cw) } else if (value.startsWith(CHANNEL_SELECTOR_URL)) { - handleChannelSelectorComms(value, parent, this.contentWindow) + this.messageChannels.push(handleChannelSelectorComms(value, parent, this.contentWindow)) } else if (value.startsWith(INTENT_RESPOLVER_URL)) { - handleIntentResolverComms(value, parent, this.contentWindow) + this.messageChannels.push(handleIntentResolverComms(value, parent, this.contentWindow)) } } } + + shutdown() { + super.shutdown() + this.messageChannels.forEach(mc => { + mc.port1.close() + mc.port2.close() + }) + } } export class MockDocument { diff --git a/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts b/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts index ab47a031d..aef88f846 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts @@ -34,10 +34,7 @@ export class MockFDC3Server extends DefaultFDC3Server { } shutdown() { - // this.theContext.forEach(i => { - // i.externalPort.close() - // i.internalPort.close() - // }) + this.tsc.shutdown() } init() { diff --git a/fdc3-for-web-implementation/packages/client/test/support/TestServerContext.ts b/fdc3-for-web-implementation/packages/client/test/support/TestServerContext.ts index dce3bfd86..37f7a4d75 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/TestServerContext.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/TestServerContext.ts @@ -49,22 +49,10 @@ export class TestServerContext implements ServerContext { this.instances = this.instances.filter(ca => ca.instanceId !== app.instanceId) } - // wrapMessagePort(mp: MessagePort) { - // return { - // postMessage(a: any, b: any) { - // mp.postMessage(a, b) - // } - - // start() { - // mp.start() - // } - - // close() { - // mp.close() - // } - - // } as MessagePortEventMap - // } + async shutdown(): Promise { + await Promise.all(this.instances.map(i => i.internalPort.close())) + await Promise.all(this.instances.map(i => i.externalPort.close())) + } async open(appId: string): Promise { const ni = this.nextInstanceId++ diff --git a/fdc3-for-web-implementation/packages/da-proxy/src/BasicDesktopAgent.ts b/fdc3-for-web-implementation/packages/da-proxy/src/BasicDesktopAgent.ts index cafee3eac..2f2fcada4 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/src/BasicDesktopAgent.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/src/BasicDesktopAgent.ts @@ -3,7 +3,7 @@ import { ChannelSupport } from "./channels/ChannelSupport"; import { AppSupport } from "./apps/AppSupport"; import { IntentSupport } from "./intents/IntentSupport"; import { HandshakeSupport } from "./handshake/HandshakeSupport"; -import { Connectable } from "./Connectable"; +import { Connectable } from "@kite9/fdc3-common"; /** * This splits out the functionality of the desktop agent into @@ -15,12 +15,14 @@ export class BasicDesktopAgent implements DesktopAgent, Connectable { readonly channels: ChannelSupport readonly intents: IntentSupport readonly apps: AppSupport + readonly connectables: Connectable[] - constructor(handshake: HandshakeSupport, channels: ChannelSupport, intents: IntentSupport, apps: AppSupport) { + constructor(handshake: HandshakeSupport, channels: ChannelSupport, intents: IntentSupport, apps: AppSupport, connectables: Connectable[]) { this.handshake = handshake this.intents = intents this.channels = channels this.apps = apps + this.connectables = connectables } async getInfo(): Promise { @@ -118,12 +120,12 @@ export class BasicDesktopAgent implements DesktopAgent, Connectable { return this.apps.getAppMetadata(app); } - disconnect(): Promise { - return this.handshake.disconnect() + async disconnect(): Promise { + await Promise.all(this.connectables.map(c => c.disconnect())) } - connect(): Promise { - return this.handshake.connect() + async connect(): Promise { + await Promise.all(this.connectables.map(c => c.connect())) } } \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/da-proxy/src/Messaging.ts b/fdc3-for-web-implementation/packages/da-proxy/src/Messaging.ts index 85996985e..b71e3b525 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/src/Messaging.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/src/Messaging.ts @@ -1,5 +1,5 @@ import { AppIdentifier, ImplementationMetadata } from "@finos/fdc3"; -import { Connectable } from "./Connectable"; +import { Connectable } from "@kite9/fdc3-common"; import { RegisterableListener } from "./listeners/RegisterableListener"; export interface Messaging extends Connectable { diff --git a/fdc3-for-web-implementation/packages/da-proxy/src/channels/DefaultChannelSupport.ts b/fdc3-for-web-implementation/packages/da-proxy/src/channels/DefaultChannelSupport.ts index 7f535fcc8..c0b6dbccd 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/src/channels/DefaultChannelSupport.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/src/channels/DefaultChannelSupport.ts @@ -26,9 +26,14 @@ const NO_OP_CHANNEL_SELECTOR: ChannelSelector = { // also does nothing }, - init: function (): Promise { + connect: function (): Promise { + return Promise.resolve() + }, + + disconnect: function (): Promise { return Promise.resolve() } + } export class DefaultChannelSupport implements ChannelSupport { diff --git a/fdc3-for-web-implementation/packages/da-proxy/src/handshake/HandshakeSupport.ts b/fdc3-for-web-implementation/packages/da-proxy/src/handshake/HandshakeSupport.ts index 424a2456a..60dd66abb 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/src/handshake/HandshakeSupport.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/src/handshake/HandshakeSupport.ts @@ -1,4 +1,4 @@ -import { Connectable } from "../Connectable" +import { Connectable } from "@kite9/fdc3-common"; import { ImplementationMetadata } from "@finos/fdc3" /** diff --git a/fdc3-for-web-implementation/packages/da-proxy/test/step-definitions/generic.steps.ts b/fdc3-for-web-implementation/packages/da-proxy/test/step-definitions/generic.steps.ts index 4269ef638..d0b17ef62 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/test/step-definitions/generic.steps.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/test/step-definitions/generic.steps.ts @@ -16,7 +16,7 @@ Given('A Desktop Agent in {string}', async function (this: CustomWorld, field: s const is = new DefaultIntentSupport(this.messaging, new SimpleIntentResolver(this)) const as = new DefaultAppSupport(this.messaging) - const da = new BasicDesktopAgent(hs, cs, is, as) + const da = new BasicDesktopAgent(hs, cs, is, as, [hs]) await da.connect() this.props[field] = da diff --git a/fdc3-for-web-implementation/packages/fdc3-common/src/ChannelSelector.ts b/fdc3-for-web-implementation/packages/fdc3-common/src/ChannelSelector.ts index 75cc1b00f..039c68320 100644 --- a/fdc3-for-web-implementation/packages/fdc3-common/src/ChannelSelector.ts +++ b/fdc3-for-web-implementation/packages/fdc3-common/src/ChannelSelector.ts @@ -1,14 +1,10 @@ import { Channel } from "@finos/fdc3" +import { Connectable } from "./Connectable" /** * Interface used by the desktop agent proxy to handle the channel selection process. */ -export interface ChannelSelector { - - /** - * Make sure the channel selector is ready to be used. - */ - init(): Promise +export interface ChannelSelector extends Connectable { /** * Called when the list of user channels is updated, or the selected channel changes. diff --git a/fdc3-for-web-implementation/packages/da-proxy/src/Connectable.ts b/fdc3-for-web-implementation/packages/fdc3-common/src/Connectable.ts similarity index 100% rename from fdc3-for-web-implementation/packages/da-proxy/src/Connectable.ts rename to fdc3-for-web-implementation/packages/fdc3-common/src/Connectable.ts diff --git a/fdc3-for-web-implementation/packages/fdc3-common/src/IntentResolver.ts b/fdc3-for-web-implementation/packages/fdc3-common/src/IntentResolver.ts index 60f1b80a4..63f375a73 100644 --- a/fdc3-for-web-implementation/packages/fdc3-common/src/IntentResolver.ts +++ b/fdc3-for-web-implementation/packages/fdc3-common/src/IntentResolver.ts @@ -1,4 +1,5 @@ import { AppIdentifier, AppIntent, Context } from "@finos/fdc3"; +import { Connectable } from "./Connectable"; export type IntentResolutionChoice = { @@ -9,12 +10,7 @@ export type IntentResolutionChoice = { /** * Interface used by the desktop agent proxy to handle the intent resolution process. */ -export interface IntentResolver { - - /** - * Make sure the intent resolver is ready to be used. - */ - init(): Promise +export interface IntentResolver extends Connectable { /** * Called when the user needs to resolve an intent. Returns either the app chosen to diff --git a/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts b/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts index 0971d32b1..7da13dda4 100644 --- a/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts +++ b/fdc3-for-web-implementation/packages/fdc3-common/src/index.ts @@ -13,4 +13,5 @@ export * from './BrowserTypes' export * from './GetAgent' export { ChannelSelector } from './ChannelSelector' -export { IntentResolver, IntentResolutionChoice } from './IntentResolver' \ No newline at end of file +export { IntentResolver, IntentResolutionChoice } from './IntentResolver' +export { Connectable } from './Connectable' \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/testing/src/agent/index.ts b/fdc3-for-web-implementation/packages/testing/src/agent/index.ts index d7f682d4a..663bea326 100644 --- a/fdc3-for-web-implementation/packages/testing/src/agent/index.ts +++ b/fdc3-for-web-implementation/packages/testing/src/agent/index.ts @@ -14,7 +14,10 @@ export class SimpleIntentResolver implements IntentResolver { this.cw = cw; } - async init(): Promise { + async connect(): Promise { + } + + async disconnect(): Promise { } async intentChosen(ir: IntentResult): Promise { From abea3af328e02d3e57f0538495d260d766e15d85 Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Thu, 29 Aug 2024 10:01:42 +0100 Subject: [PATCH 06/11] Cleaning up of server responses --- .../src/messaging/MessagePortMessaging.ts | 13 ++ .../client/src/messaging/message-port.ts | 2 +- .../features/desktop-agent-strategy.feature | 2 +- .../step-definitions/desktop-agent.steps.ts | 47 ++--- .../client/test/support/MockFDC3Server.ts | 37 ++-- .../client/test/support/TestMessaging.ts | 182 +++++++++--------- .../support/responses/AutomaticResponses.ts | 10 + .../test/support/responses/CurrentChannel.ts | 11 +- .../test/support/responses/FindIntent.ts | 9 +- .../test/support/responses/Handshake.ts | 9 +- .../test/support/responses/RaiseIntent.ts | 10 +- .../test/support/responses/UserChannels.ts | 10 +- .../packages/client/test/world/index.ts | 3 - 13 files changed, 192 insertions(+), 153 deletions(-) create mode 100644 fdc3-for-web-implementation/packages/client/test/support/responses/AutomaticResponses.ts diff --git a/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts b/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts index 316e70efe..3a7140499 100644 --- a/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts +++ b/fdc3-for-web-implementation/packages/client/src/messaging/MessagePortMessaging.ts @@ -56,5 +56,18 @@ export class MessagePortMessaging extends AbstractWebMessaging { "source": this.getSource() } } + + waitFor(filter: (m: any) => boolean, timeoutErrorMessage?: string): Promise { + console.log("Waiting for", filter, timeoutErrorMessage) + return super.waitFor(filter, timeoutErrorMessage).then((v: any) => { + console.log("Wait over ", v, timeoutErrorMessage) + return v; + }) + } + + async disconnect(): Promise { + await super.disconnect() + this.cd.messagePort.close() + } } diff --git a/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts b/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts index b9dae6c04..17ae83634 100644 --- a/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts +++ b/fdc3-for-web-implementation/packages/client/src/messaging/message-port.ts @@ -42,7 +42,7 @@ export async function createDesktopAgentAPI(cd: ConnectionDetails): Promise { wtf.dump() }, 10000) + // setTimeout(() => { + // //console.log((process as any)._getActiveHandles()) + // wtf.dump() + // }, 10000) }) When('I call getAgentAPI for a promise result with the following options', function (this: CustomWorld, dt: DataTable) { diff --git a/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts b/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts index aef88f846..77879241a 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/MockFDC3Server.ts @@ -1,8 +1,13 @@ -import { BasicDirectory, DefaultFDC3Server } from "@kite9/da-server" -import { WebConnectionProtocol2LoadURL, WebConnectionProtocol3Handshake } from "@kite9/fdc3-common" +import { FDC3Server } from "@kite9/da-server" +import { AppRequestMessage, WebConnectionProtocol2LoadURL, WebConnectionProtocol3Handshake } from "@kite9/fdc3-common" import { TestServerContext } from "./TestServerContext" import { MockWindow } from "./MockDocument" -import { ChannelState, ChannelType } from "@kite9/da-server/src/handlers/BroadcastHandler" +import { AutomaticResponse } from "./responses/AutomaticResponses" +import { FindIntent } from "./responses/FindIntent" +import { RaiseIntent } from "./responses/RaiseIntent" +import { Handshake } from "./responses/Handshake" +import { UserChannels } from "./responses/UserChannels" +import { CurrentChannel } from "./responses/CurrentChannel" export const dummyInstanceId = { appId: "Test App Id", instanceId: "1" } @@ -12,27 +17,35 @@ export const CHANNEL_SELECTOR_URL = "https://mock.fdc3.com/channelSelector" export const INTENT_RESPOLVER_URL = "https://mock.fdc3.com/resolver" -export class MockFDC3Server extends DefaultFDC3Server { +export class MockFDC3Server implements FDC3Server { private useIframe: boolean private window: MockWindow private tsc: TestServerContext - constructor(window: MockWindow, useIframe: boolean, ctx: TestServerContext) { - const channelDetails: ChannelState[] = [ - { id: "one", type: ChannelType.user, context: [], displayMetadata: { name: "THE RED CHANNEL", color: "red" } }, - { id: "two", type: ChannelType.user, context: [], displayMetadata: { name: "THE BLUE CHANNEL", color: "blue" } }, - { id: "three", type: ChannelType.user, context: [], displayMetadata: { name: "THE GREEN CHANNEL", color: "green" } } - ] + readonly automaticResponses: AutomaticResponse[] = [ + new FindIntent(), + new RaiseIntent(), + new Handshake(), + new UserChannels(), + new CurrentChannel() + ] - const dir = new BasicDirectory([dummyInstanceId]) - super(ctx, dir, channelDetails) + constructor(window: MockWindow, useIframe: boolean, ctx: TestServerContext) { this.useIframe = useIframe this.window = window this.tsc = ctx this.init() } + receive(message: AppRequestMessage, from: string): void { + this.automaticResponses.forEach((r) => { + if (r.filter(message.type)) { + r.action(message, this.tsc, from) + } + }) + } + shutdown() { this.tsc.shutdown() } diff --git a/fdc3-for-web-implementation/packages/client/test/support/TestMessaging.ts b/fdc3-for-web-implementation/packages/client/test/support/TestMessaging.ts index 51e65d93e..f16c6be7f 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/TestMessaging.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/TestMessaging.ts @@ -1,94 +1,88 @@ -import { AppIdentifier } from "@finos/fdc3"; -import { RegisterableListener } from "@kite9/da-proxy"; -import { AppRequestMessage } from "@kite9/fdc3-common"; -import { v4 as uuidv4 } from 'uuid' -import { AbstractWebMessaging } from "../../src/messaging/AbstractWebMessaging"; -import { CurrentChannel } from "./responses/CurrentChannel"; -import { FindIntent } from "./responses/FindIntent"; -import { Handshake } from "./responses/Handshake"; -import { RaiseIntent } from "./responses/RaiseIntent"; -import { UserChannels } from "./responses/UserChannels"; - - -export interface AutomaticResponse { - - filter: (t: string) => boolean, - action: (input: object, m: TestMessaging) => Promise - -} - -export class TestMessaging extends AbstractWebMessaging { - - readonly listeners: Map = new Map() - readonly allPosts: AppRequestMessage[] = [] - - constructor() { - super({ - channelSelector: true, - intentResolver: true, - timeout: 2000, - dontSetWindowFdc3: false - }, 'abc123') - } - - readonly automaticResponses: AutomaticResponse[] = [ - new FindIntent(), - new RaiseIntent(), - new Handshake(), - new UserChannels(), - new CurrentChannel() - ] - - register(l: RegisterableListener) { - this.listeners.set(l.id!!, l) - } - - unregister(id: string) { - this.listeners.delete(id) - } - - createMeta() { - return { - "requestUuid": this.createUUID(), - "timestamp": new Date(), - "source": this.getSource(), - "responseUuid": this.createUUID() - } - } - - - getSource(): AppIdentifier { - return { - appId: "SomeDummyApp", - instanceId: "some.dummy.instance" - } - } - - createUUID(): string { - return uuidv4() - } - - - post(message: AppRequestMessage): Promise { - this.allPosts.push(message) - for (let i = 0; i < this.automaticResponses.length; i++) { - const ar = this.automaticResponses[i] - if (ar.filter(message.type)) { - return ar.action(message, this) - } - } - - return Promise.resolve(); - } - - receive(m: any) { - this.listeners.forEach((v, k) => { - if (v.filter(m)) { - console.log("Processing in " + k) - v.action(m) - } else { - console.log("Ignoring in " + k) - } - }) - } -} \ No newline at end of file +// import { AppIdentifier } from "@finos/fdc3"; +// import { RegisterableListener } from "@kite9/da-proxy"; +// import { AppRequestMessage } from "@kite9/fdc3-common"; +// import { v4 as uuidv4 } from 'uuid' +// import { AbstractWebMessaging } from "../../src/messaging/AbstractWebMessaging"; +// import { CurrentChannel } from "./responses/CurrentChannel"; +// import { FindIntent } from "./responses/FindIntent"; +// import { Handshake } from "./responses/Handshake"; +// import { RaiseIntent } from "./responses/RaiseIntent"; +// import { UserChannels } from "./responses/UserChannels"; + + + +// export class TestMessaging extends AbstractWebMessaging { + +// readonly listeners: Map = new Map() +// readonly allPosts: AppRequestMessage[] = [] + +// constructor() { +// super({ +// channelSelector: true, +// intentResolver: true, +// timeout: 2000, +// dontSetWindowFdc3: false +// }, 'abc123') +// } + +// readonly automaticResponses: AutomaticResponse[] = [ +// new FindIntent(), +// new RaiseIntent(), +// new Handshake(), +// new UserChannels(), +// new CurrentChannel() +// ] + +// register(l: RegisterableListener) { +// this.listeners.set(l.id!!, l) +// } + +// unregister(id: string) { +// this.listeners.delete(id) +// } + +// createMeta() { +// return { +// "requestUuid": this.createUUID(), +// "timestamp": new Date(), +// "source": this.getSource(), +// "responseUuid": this.createUUID() +// } +// } + + +// getSource(): AppIdentifier { +// return { +// appId: "SomeDummyApp", +// instanceId: "some.dummy.instance" +// } +// } + +// createUUID(): string { +// return uuidv4() +// } + + +// post(message: AppRequestMessage): Promise { +// this.allPosts.push(message) +// for (let i = 0; i < this.automaticResponses.length; i++) { +// const ar = this.automaticResponses[i] +// if (ar.filter(message.type)) { +// return ar.action(message, this) +// } +// } + +// return Promise.resolve(); +// } + +// receive(m: any) { +// this.listeners.forEach((v, k) => { +// if (v.filter(m)) { +// console.log("Processing in " + k) +// v.action(m) +// } else { +// console.log("Ignoring in " + k) +// } +// }) +// } +// } \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/AutomaticResponses.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/AutomaticResponses.ts new file mode 100644 index 000000000..de364ced3 --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/AutomaticResponses.ts @@ -0,0 +1,10 @@ +import { InstanceID } from "@kite9/da-server"; +import { TestServerContext } from "../TestServerContext"; + +export interface AutomaticResponse { + + filter: (t: string) => boolean, + action: (input: object, m: TestServerContext, from: InstanceID) => Promise + +} + diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/CurrentChannel.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/CurrentChannel.ts index 556dfb6ea..822a0c820 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/responses/CurrentChannel.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/CurrentChannel.ts @@ -1,5 +1,7 @@ -import { AutomaticResponse, TestMessaging } from "../TestMessaging"; import { GetCurrentChannelRequest, GetCurrentChannelResponse } from "@kite9/fdc3-common"; +import { TestServerContext } from "../TestServerContext"; +import { InstanceID } from "@kite9/da-server"; +import { AutomaticResponse } from "./AutomaticResponses"; export class CurrentChannel implements AutomaticResponse { @@ -7,14 +9,13 @@ export class CurrentChannel implements AutomaticResponse { return t == 'getCurrentChannelRequest' } - action(input: object, m: TestMessaging) { + action(input: object, m: TestServerContext, from: InstanceID) { const out = this.createResponse(input as GetCurrentChannelRequest, m) - - setTimeout(() => { m.receive(out) }, 100) + setTimeout(() => { m.post(out, from) }, 100) return Promise.resolve() } - private createResponse(i: GetCurrentChannelRequest, m: TestMessaging): GetCurrentChannelResponse { + private createResponse(i: GetCurrentChannelRequest, m: TestServerContext): GetCurrentChannelResponse { return { meta: { ...i.meta, diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/FindIntent.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/FindIntent.ts index cb55e5c13..be2fc3757 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/responses/FindIntent.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/FindIntent.ts @@ -1,5 +1,8 @@ import { FindIntentRequest, FindIntentResponse } from "@kite9/fdc3-common"; -import { AutomaticResponse, TestMessaging } from "../TestMessaging"; +import { TestServerContext } from "../TestServerContext"; +import { InstanceID } from "@kite9/da-server"; +import { AutomaticResponse } from "./AutomaticResponses"; + export class FindIntent implements AutomaticResponse { @@ -7,10 +10,10 @@ export class FindIntent implements AutomaticResponse { return t == 'findIntentRequest' } - action(input: object, m: TestMessaging) { + action(input: object, m: TestServerContext, from: InstanceID) { const intentRequest = input as FindIntentRequest const request = this.createFindIntentResponseMessage(intentRequest) - setTimeout(() => { m.receive(request) }, 100) + setTimeout(() => { m.post(request, from) }, 100) return Promise.resolve() } diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/Handshake.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/Handshake.ts index 88bc495a9..d422035be 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/responses/Handshake.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/Handshake.ts @@ -1,4 +1,7 @@ -import { AutomaticResponse, TestMessaging } from "../TestMessaging"; +import { TestServerContext } from "../TestServerContext"; +import { InstanceID } from "@kite9/da-server"; +import { AutomaticResponse } from "./AutomaticResponses"; + import { WebConnectionProtocol4ValidateAppIdentity, WebConnectionProtocol5ValidateAppIdentitySuccessResponse } from "@kite9/fdc3-common"; export class Handshake implements AutomaticResponse { @@ -7,10 +10,10 @@ export class Handshake implements AutomaticResponse { return t == 'WCP4ValidateAppIdentity' } - action(input: object, m: TestMessaging) { + action(input: object, m: TestServerContext, from: InstanceID) { const out = this.createResponse(input as WebConnectionProtocol4ValidateAppIdentity) - setTimeout(() => { m.receive(out) }, 100) + setTimeout(() => { m.post(out, from) }, 100) return Promise.resolve() } diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/RaiseIntent.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/RaiseIntent.ts index da33d9164..862f82a00 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/responses/RaiseIntent.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/RaiseIntent.ts @@ -1,5 +1,7 @@ import { RaiseIntentRequest, RaiseIntentResponse } from "@kite9/fdc3-common"; -import { AutomaticResponse, TestMessaging } from "../TestMessaging"; +import { TestServerContext } from "../TestServerContext"; +import { InstanceID } from "@kite9/da-server"; +import { AutomaticResponse } from "./AutomaticResponses"; export class RaiseIntent implements AutomaticResponse { @@ -8,7 +10,7 @@ export class RaiseIntent implements AutomaticResponse { return t == 'raiseIntentRequest' } - createRaiseIntentAgentResponseMessage(intentRequest: RaiseIntentRequest, m: TestMessaging): RaiseIntentResponse { + createRaiseIntentAgentResponseMessage(intentRequest: RaiseIntentRequest, m: TestServerContext): RaiseIntentResponse { const out: RaiseIntentResponse = { meta: { ...intentRequest.meta, @@ -26,11 +28,11 @@ export class RaiseIntent implements AutomaticResponse { return out } - action(input: object, m: TestMessaging) { + action(input: object, m: TestServerContext, from: InstanceID) { const intentRequest = input as RaiseIntentRequest // this sends out the intent resolution const out1 = this.createRaiseIntentAgentResponseMessage(intentRequest, m) - setTimeout(() => { m.receive(out1) }, 100) + setTimeout(() => { m.post(out1, from) }, 100) return Promise.resolve() } } diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts index d0c4052e8..e881544a9 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts @@ -1,4 +1,6 @@ -import { AutomaticResponse, TestMessaging } from "../TestMessaging"; +import { TestServerContext } from "../TestServerContext"; +import { InstanceID } from "@kite9/da-server"; +import { AutomaticResponse } from "./AutomaticResponses"; import { GetUserChannelsRequest, GetUserChannelsResponse } from "@kite9/fdc3-common"; export class UserChannels implements AutomaticResponse { @@ -7,14 +9,14 @@ export class UserChannels implements AutomaticResponse { return t == 'getUserChannelsRequest' } - action(input: object, m: TestMessaging) { + action(input: object, m: TestServerContext, from: InstanceID) { const out = this.createResponse(input as GetUserChannelsRequest, m) - setTimeout(() => { m.receive(out) }, 100) + setTimeout(() => { m.post(out, from) }, 100) return Promise.resolve() } - private createResponse(i: GetUserChannelsRequest, m: TestMessaging): GetUserChannelsResponse { + private createResponse(i: GetUserChannelsRequest, m: TestServerContext): GetUserChannelsResponse { return { meta: { ...i.meta, diff --git a/fdc3-for-web-implementation/packages/client/test/world/index.ts b/fdc3-for-web-implementation/packages/client/test/world/index.ts index 9653ed5b9..e9daf8dfb 100644 --- a/fdc3-for-web-implementation/packages/client/test/world/index.ts +++ b/fdc3-for-web-implementation/packages/client/test/world/index.ts @@ -1,13 +1,10 @@ import { setWorldConstructor } from "@cucumber/cucumber"; import { PropsWorld } from "@kite9/testing"; import { MockFDC3Server } from "../support/MockFDC3Server"; -import { TestMessaging } from "../support/TestMessaging"; import { TestServerContext } from "../support/TestServerContext"; export class CustomWorld extends PropsWorld { - messaging: TestMessaging | null = null - mockFDC3Server: MockFDC3Server | null = null mockContext: TestServerContext = new TestServerContext(this) From c1a64e09ed40aece54b5aae9094c0c81acd0d144 Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Thu, 29 Aug 2024 10:12:22 +0100 Subject: [PATCH 07/11] Connection tests working --- .../features/desktop-agent-strategy.feature | 140 +++++++++--------- .../step-definitions/desktop-agent.steps.ts | 43 +++--- 2 files changed, 94 insertions(+), 89 deletions(-) diff --git a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature index 4b0dd1b66..aefdd874d 100644 --- a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature +++ b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature @@ -26,70 +26,76 @@ Feature: Different Strategies for Accessing the Desktop Agent | message | WCP3Handshake | Then I call "{document}" with "shutdown" And I call "{desktopAgent}" with "disconnect" - # Scenario: Running inside a Browser using the embedded iframe strategy - # Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response - # And we wait for a period of "200" ms - # And I call getAgentAPI for a promise result with the following options - # | dontSetWindowFdc3 | timeout | - # | false | 8000 | - # And I refer to "{result}" as "theAPIPromise" - # Then the promise "{theAPIPromise}" should resolve - # And I refer to "{result}" as "desktopAgent" - # And I call "{desktopAgent}" with "getInfo" - # Then "{result}" is an object with the following contents - # | fdc3Version | appMetadata.appId | provider | - # | 2.0 | Test App Id | cucumber-provider | - # And I refer to "{document.iframes[0]}" as "embedded-iframe" - # Then "{embedded-iframe}" is an object with the following contents - # | tag | atts.name | style.width | style.height | - # | iframe | FDC3 Communications | 0px | 0px | - # And I refer to "{document.iframes[1]}" as "intent-resolver-iframe" - # And I refer to "{document.iframes[2]}" as "channel-selector-iframe" - # Then "{channel-selector-iframe}" is an object with the following contents - # | tag | atts.name | atts.src | style.width | style.height | - # | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | - # Then "{intent-resolver-iframe}" is an object with the following contents - # | tag | atts.name | atts.src | style.width | style.height | - # | iframe | FDC3 Intent Resolver | https://mock.fdc3.com/resolver | 100% | 100% | - # And "{window.fdc3}" is not null - # And "{window.events}" is an array of objects with the following contents - # | type | data.type | - # | message | WCP1Hello | - # | message | WCP2LoadUrl | - # | message | WCP3Handshake | - # | message | iframeHello | - # | message | iframeHello | - # | fdc3Ready | {null} | - # Then I call "{document}" with "shutdown" - # And I call "{desktopAgent}" with "disconnect" - # Scenario: Running inside an Electron Container. - # In this scenario, window.fdc3 is set by the electron container and returned by getAgentAPI - # Given A Dummy Desktop Agent in "dummy-api" - # And I call getAgentAPI for a promise result - # And I refer to "{result}" as "theAPIPromise" - # And we wait for a period of "500" ms - # And `window.fdc3` is injected into the runtime with the value in "{dummy-api}" - # Then the promise "{theAPIPromise}" should resolve - # And I call "{result}" with "getInfo" - # Then "{result}" is an object with the following contents - # | fdc3Version | appMetadata.appId | provider | - # | 2.0 | cucumber-app | cucumber-provider | - # Then I call "{document}" with "shutdown" - # Scenario: Failover Strategy. - # Given A Dummy Desktop Agent in "dummy-api" - # And "dummyFailover" is a function which returns a promise of "{dummy-api}" - # And I call getAgentAPI for a promise result with the following options - # | failover | timeout | - # | {dummyFailover} | 8000 | - # And I refer to "{result}" as "theAPIPromise" - # Then the promise "{theAPIPromise}" should resolve - # And I call "{result}" with "getInfo" - # Then "{result}" is an object with the following contents - # | fdc3Version | appMetadata.appId | provider | - # | 2.0 | cucumber-app | cucumber-provider | - # Then I call "{document}" with "shutdown" - # Scenario: Recovery from SessionState - # Here, we recover the details of the session from the session state, obviating the need to - # make a request to the parent iframe. - # Scenario: Failed Recovery from SessionState - # App tries to recover with an ID that doesn't exist. + + Scenario: Running inside a Browser using the embedded iframe strategy + Given Parent Window desktop "da" listens for postMessage events in "{window}", returns iframe response + And we wait for a period of "200" ms + And I call getAgentAPI for a promise result with the following options + | dontSetWindowFdc3 | timeout | + | false | 8000 | + And I refer to "{result}" as "theAPIPromise" + Then the promise "{theAPIPromise}" should resolve + And I refer to "{result}" as "desktopAgent" + And I call "{desktopAgent}" with "getInfo" + Then "{result}" is an object with the following contents + | fdc3Version | appMetadata.appId | provider | + | 2.0 | cucumber-app | cucumber-provider | + And I refer to "{document.iframes[0]}" as "embedded-iframe" + Then "{embedded-iframe}" is an object with the following contents + | tag | atts.name | style.width | style.height | + | iframe | FDC3 Communications | 0px | 0px | + And I refer to "{document.iframes[1]}" as "intent-resolver-iframe" + And I refer to "{document.iframes[2]}" as "channel-selector-iframe" + Then "{channel-selector-iframe}" is an object with the following contents + | tag | atts.name | atts.src | style.width | style.height | + | iframe | FDC3 Channel Selector | https://mock.fdc3.com/channelSelector | 100% | 100% | + Then "{intent-resolver-iframe}" is an object with the following contents + | tag | atts.name | atts.src | style.width | style.height | + | iframe | FDC3 Intent Resolver | https://mock.fdc3.com/resolver | 100% | 100% | + And "{window.fdc3}" is not null + And "{window.events}" is an array of objects with the following contents + | type | data.type | + | message | WCP1Hello | + | message | WCP2LoadUrl | + | message | WCP3Handshake | + | message | iframeHello | + | message | iframeHello | + | fdc3Ready | {null} | + Then I call "{document}" with "shutdown" + And I call "{desktopAgent}" with "disconnect" + + Scenario: Running inside an Electron Container. + In this scenario, window.fdc3 is set by the electron container and returned by getAgentAPI + + Given A Dummy Desktop Agent in "dummy-api" + And I call getAgentAPI for a promise result + And I refer to "{result}" as "theAPIPromise" + And we wait for a period of "500" ms + And `window.fdc3` is injected into the runtime with the value in "{dummy-api}" + Then the promise "{theAPIPromise}" should resolve + And I call "{result}" with "getInfo" + Then "{result}" is an object with the following contents + | fdc3Version | appMetadata.appId | provider | + | 2.0 | cucumber-app | cucumber-provider | + Then I call "{document}" with "shutdown" + + Scenario: Failover Strategy. + Given A Dummy Desktop Agent in "dummy-api" + And "dummyFailover" is a function which returns a promise of "{dummy-api}" + And I call getAgentAPI for a promise result with the following options + | failover | timeout | + | {dummyFailover} | 1000 | + And I refer to "{result}" as "theAPIPromise" + Then the promise "{theAPIPromise}" should resolve + And I call "{result}" with "getInfo" + Then "{result}" is an object with the following contents + | fdc3Version | appMetadata.appId | provider | + | 2.0 | cucumber-app | cucumber-provider | + Then I call "{document}" with "shutdown" + + Scenario: Recovery from SessionState + Here, we recover the details of the session from the session state, obviating the need to + make a request to the parent iframe. + + Scenario: Failed Recovery from SessionState + App tries to recover with an ID that doesn't exist. diff --git a/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts b/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts index 69845ce7e..1ab9b4f61 100644 --- a/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts +++ b/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts @@ -10,7 +10,8 @@ import { dummyInstanceId, MockFDC3Server } from '../support/MockFDC3Server'; // import { DefaultDesktopAgentChannelSelector } from '../../src/ui/DefaultDesktopAgentChannelSelector'; // import { NoopAppSupport } from '../../src/apps/NoopAppSupport'; import { MockStorage } from '../support/MockStorage'; -//var wtf = require('wtfnode') +import { DesktopAgent, ImplementationMetadata } from '@finos/fdc3'; +var wtf = require('wtfnode') setupGenericSteps() Given('Parent Window desktop {string} listens for postMessage events in {string}, returns direct message response', async function (this: CustomWorld, field: string, w: string) { @@ -28,25 +29,23 @@ Given('Parent Window desktop {string} listens for postMessage events in {string} }) -// Given('A Dummy Desktop Agent in {string}', async function (this: CustomWorld, field: string) { +Given('A Dummy Desktop Agent in {string}', async function (this: CustomWorld, field: string) { -// if (!this.messaging) { -// this.messaging = new TestMessaging(); -// } + const da: DesktopAgent = { + async getInfo(): Promise { + return { + fdc3Version: "2.0", + appMetadata: { + appId: "cucumber-app" + }, + provider: "cucumber-provider" + } as any + } + } as any -// const intentResolver = new DefaultDesktopAgentIntentResolver("https://localhost:8080/dummy-intent-resolver.html") -// const channelSelector = new DefaultDesktopAgentChannelSelector("https://localhost:8080/dummy-channel-selector.html") -// const cs = new DefaultChannelSupport(this.messaging, channelSelector) -// const hs = new DefaultHandshakeSupport(this.messaging) -// const is = new DefaultIntentSupport(this.messaging, intentResolver) -// const as = new NoopAppSupport(this.messaging) - -// const da = new BasicDesktopAgent(hs, cs, is, as, [hs, intentResolver, channelSelector]) -// await da.connect() - -// this.props[field] = da -// this.props['result'] = null -// }) + this.props[field] = da + this.props['result'] = null +}) Given('`window.fdc3` is injected into the runtime with the value in {string}', async function (this: CustomWorld, field: string) { const object = handleResolve(field, this) @@ -64,10 +63,10 @@ When('I call getAgentAPI for a promise result', function (this: CustomWorld) { After(function (this: CustomWorld) { console.log("Cleaning up") - // setTimeout(() => { - // //console.log((process as any)._getActiveHandles()) - // wtf.dump() - // }, 10000) + setTimeout(() => { + //console.log((process as any)._getActiveHandles()) + wtf.dump() + }, 10000) }) When('I call getAgentAPI for a promise result with the following options', function (this: CustomWorld, dt: DataTable) { From b1f0679a49dd01eebeb3bf12a537cb2211d880f3 Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Thu, 29 Aug 2024 11:05:08 +0100 Subject: [PATCH 08/11] Channel selector tests added --- .../features/default-channel-selector.feature | 20 ++++++++++ .../channel-selector.steps.ts | 32 ++++++++++++++++ .../client/test/support/FrameTypes.ts | 38 ++++++++++++++++++- .../client/test/support/MockDocument.ts | 4 +- .../test/support/responses/UserChannels.ts | 31 +++++++-------- .../default-channel-selector.feature | 13 ------- .../desktop-agent-strategy.feature | 0 7 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 fdc3-for-web-implementation/packages/client/test/features/default-channel-selector.feature delete mode 100644 fdc3-for-web-implementation/packages/client/test/unused-features/default-channel-selector.feature rename fdc3-for-web-implementation/packages/client/test/{features => unused-features}/desktop-agent-strategy.feature (100%) diff --git a/fdc3-for-web-implementation/packages/client/test/features/default-channel-selector.feature b/fdc3-for-web-implementation/packages/client/test/features/default-channel-selector.feature new file mode 100644 index 000000000..824820abe --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/test/features/default-channel-selector.feature @@ -0,0 +1,20 @@ +Feature: Default Channel Selector + + Background: Desktop Agent API + Given a browser document in "document" and window in "window" + And A Channel Selector in "channel-selector" with callback piping to "cb" + Given User Channels one, two and three in "channel-list" + + Scenario: Channel Selector Requests Channel Change + Given The channel selector sends a channel change message for channel "one" + And we wait for a period of "200" ms + Then "{cb}" is "one" + And I call "{document}" with "shutdown" + + Scenario: Updating channel information in the channel selector + Given I call "{channel-selector}" with "updateChannel" with parameters "one" and "{channel-list}" + And we wait for a period of "200" ms + Then "{lastChannelSelectorMessage}" is an object with the following contents + | type | payload.selected | payload.userChannels[0].id | payload.userChannels[1].id | payload.userChannels[2].id | + | iframeChannels | one | one | two | three | + And I call "{document}" with "shutdown" diff --git a/fdc3-for-web-implementation/packages/client/test/step-definitions/channel-selector.steps.ts b/fdc3-for-web-implementation/packages/client/test/step-definitions/channel-selector.steps.ts index e69de29bb..97dd30a02 100644 --- a/fdc3-for-web-implementation/packages/client/test/step-definitions/channel-selector.steps.ts +++ b/fdc3-for-web-implementation/packages/client/test/step-definitions/channel-selector.steps.ts @@ -0,0 +1,32 @@ +import { Given } from "@cucumber/cucumber"; +import { handleResolve } from "@kite9/testing"; +import { DefaultDesktopAgentChannelSelector } from "../../src/ui/DefaultDesktopAgentChannelSelector"; +import { CHANNEL_SELECTOR_URL } from "../support/MockFDC3Server"; +import { USER_CHANNELS } from "../support/responses/UserChannels"; +import { CustomWorld } from "../world"; + +Given('A Channel Selector in {string} with callback piping to {string}', async function (this: CustomWorld, field: string, cb: string) { + const cs = new DefaultDesktopAgentChannelSelector(CHANNEL_SELECTOR_URL); + + cs.setChannelChangeCallback((channelId: string) => { + this.props[cb] = channelId + }) + + this.props[field] = cs + await cs.connect() +}) + +Given('User Channels one, two and three in {string}', function (this: CustomWorld, field: string) { + this.props[field] = USER_CHANNELS +}) + +Given('The channel selector sends a channel change message for channel {string}', async function (this: CustomWorld, channel: string) { + const port = handleResolve("{document.iframes[0].messageChannels[0].port2}", this) + + port.postMessage({ + type: 'iframeChannelSelected', + payload: { + selected: channel + } + }) +}) \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts b/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts index fa5219856..10e0bc44c 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/FrameTypes.ts @@ -30,7 +30,7 @@ export function handleEmbeddedIframeComms(value: string, parent: MockWindow, cw: } } -export function handleChannelSelectorComms(_value: string, parent: MockWindow, source: Window): MessageChannel { +export function handleChannelSelectorComms(_value: string, parent: MockWindow, source: Window, cw: CustomWorld): MessageChannel { const connection = new MessageChannel(); try { parent.dispatchEvent({ @@ -42,6 +42,23 @@ export function handleChannelSelectorComms(_value: string, parent: MockWindow, s source, ports: [connection.port1] } as any as Event) + + + connection.port2.onmessage = (e) => { + if (e.data.type == 'iframeHandshake') { + setTimeout(() => { + connection.port2.postMessage({ + type: 'iframeRestyle', + payload: { + css: { + "width": "100px" + } + } + }) + }, 100) + } + cw.props['lastChannelSelectorMessage'] = e.data + } } catch (e) { console.error(e) } @@ -49,7 +66,7 @@ export function handleChannelSelectorComms(_value: string, parent: MockWindow, s return connection } -export function handleIntentResolverComms(_value: string, parent: MockWindow, source: Window): MessageChannel { +export function handleIntentResolverComms(_value: string, parent: MockWindow, source: Window, cw: CustomWorld): MessageChannel { const connection = new MessageChannel(); try { parent.dispatchEvent({ @@ -61,6 +78,23 @@ export function handleIntentResolverComms(_value: string, parent: MockWindow, so source, ports: [connection.port1] } as any as Event) + + connection.port2.onmessage = (e) => { + if (e.type == 'iframeHandshake') { + setTimeout(() => { + connection.port2.postMessage({ + type: 'iframeRestyle', + payload: { + css: { + "width": "100px" + } + } + }) + }, 100) + } + + cw.props['lastIntentResolverMessage'] = e + } } catch (e) { console.error(e) } diff --git a/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts b/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts index 6662a5814..c494c4cde 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/MockDocument.ts @@ -133,9 +133,9 @@ class MockIFrame extends MockWindow { if (value.startsWith(EMBED_URL)) { handleEmbeddedIframeComms(value, parent, this.cw) } else if (value.startsWith(CHANNEL_SELECTOR_URL)) { - this.messageChannels.push(handleChannelSelectorComms(value, parent, this.contentWindow)) + this.messageChannels.push(handleChannelSelectorComms(value, parent, this.contentWindow, this.cw)) } else if (value.startsWith(INTENT_RESPOLVER_URL)) { - this.messageChannels.push(handleIntentResolverComms(value, parent, this.contentWindow)) + this.messageChannels.push(handleIntentResolverComms(value, parent, this.contentWindow, this.cw)) } } } diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts index e881544a9..e55af6066 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/UserChannels.ts @@ -3,6 +3,21 @@ import { InstanceID } from "@kite9/da-server"; import { AutomaticResponse } from "./AutomaticResponses"; import { GetUserChannelsRequest, GetUserChannelsResponse } from "@kite9/fdc3-common"; +export const USER_CHANNELS = [ + { + id: "one", + type: "user" + }, + { + id: "two", + type: "user" + }, + { + id: "three", + type: "user" + } +] as any + export class UserChannels implements AutomaticResponse { filter(t: string) { @@ -24,21 +39,7 @@ export class UserChannels implements AutomaticResponse { }, type: "getUserChannelsResponse", payload: { - userChannels: [ - { - id: "one", - type: "user" - }, - { - id: "two", - type: "user" - }, - { - id: "three", - type: "user" - } - ] - + userChannels: USER_CHANNELS } } } diff --git a/fdc3-for-web-implementation/packages/client/test/unused-features/default-channel-selector.feature b/fdc3-for-web-implementation/packages/client/test/unused-features/default-channel-selector.feature deleted file mode 100644 index 367f2e2ff..000000000 --- a/fdc3-for-web-implementation/packages/client/test/unused-features/default-channel-selector.feature +++ /dev/null @@ -1,13 +0,0 @@ -Feature: Default Channel Selector - - Background: Desktop Agent API - Given a browser document in "document" and window in "window" - And A Dummy Desktop Agent in "dummy-api" - And Testing ends after "5000" ms - - Scenario: App Requests Channel Change - Given "{document.iframes[0]}" receives a "SelectorMessageInitialize" message for the channel selector and pipes comms to "output" - And we wait for a period of "200" ms - Then "{output}" is an array of objects with the following contents - | type | data.type | data.channelId | data.channels[0].id | data.channels[1].id | data.channels[2].id | - | message | SelectorMessageChannels | {null} | one | two | three | diff --git a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature b/fdc3-for-web-implementation/packages/client/test/unused-features/desktop-agent-strategy.feature similarity index 100% rename from fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature rename to fdc3-for-web-implementation/packages/client/test/unused-features/desktop-agent-strategy.feature From 80ec84883aad5568217092c4a79ee44c716b2b76 Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Thu, 29 Aug 2024 11:30:46 +0100 Subject: [PATCH 09/11] Fixed tests for intent resolver --- .../client/src/ui/NullIIntentResolver.ts | 3 +- .../features/default-intent-resolver.feature | 29 +++++ .../desktop-agent-strategy.feature | 0 .../step-definitions/intent-resolver.steps.ts | 102 ++++++++++++++---- .../default-intent-resolver.feature | 25 ----- 5 files changed, 109 insertions(+), 50 deletions(-) create mode 100644 fdc3-for-web-implementation/packages/client/test/features/default-intent-resolver.feature rename fdc3-for-web-implementation/packages/client/test/{unused-features => features}/desktop-agent-strategy.feature (100%) delete mode 100644 fdc3-for-web-implementation/packages/client/test/unused-features/default-intent-resolver.feature diff --git a/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts b/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts index dc989f32c..ad74977d4 100644 --- a/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts +++ b/fdc3-for-web-implementation/packages/client/src/ui/NullIIntentResolver.ts @@ -11,7 +11,6 @@ export class NullIntentResolver implements IntentResolver { async connect(): Promise { } - chooseIntent(_appIntents: AppIntent[], _ctx: Context): Promise { - throw new Error("Method not implemented."); + async chooseIntent(_appIntents: AppIntent[], _ctx: Context): Promise { } } \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/test/features/default-intent-resolver.feature b/fdc3-for-web-implementation/packages/client/test/features/default-intent-resolver.feature new file mode 100644 index 000000000..98047b309 --- /dev/null +++ b/fdc3-for-web-implementation/packages/client/test/features/default-intent-resolver.feature @@ -0,0 +1,29 @@ +Feature: Default Intent Resolver + + Background: Desktop Agent API + Given a browser document in "document" and window in "window" + And An Intent Resolver in "intent-resolver" + And "instrumentContext" is a "fdc3.instrument" context + And "appIntents" is an AppIntents array with a ViewNews intent and two apps + + Scenario: App Requests Intent Resolution + Given I call "{intent-resolver}" with "chooseIntent" with parameters "{appIntents}" and "{instrumentContext}" for a promise + And I refer to "{result}" as "theIntentPromise" + And we wait for a period of "200" ms + Given The intent resolver sends an intent selection message + Then the promise "{theIntentPromise}" should resolve + And "{result}" is an object with the following contents + | intent | appId.appId | + | ViewNews | app1 | + And I call "{document}" with "shutdown" + And I call "{intent-resolver}" with "disconnect" + + Scenario: Intent Resolution Cancelled + Given I call "{intent-resolver}" with "chooseIntent" with parameters "{appIntents}" and "{instrumentContext}" for a promise + And I refer to "{result}" as "theIntentPromise" + And we wait for a period of "200" ms + Given The intent resolver cancels the intent selection message + Then the promise "{theIntentPromise}" should resolve + And "{result}" is undefined + And I call "{document}" with "shutdown" + And I call "{intent-resolver}" with "disconnect" diff --git a/fdc3-for-web-implementation/packages/client/test/unused-features/desktop-agent-strategy.feature b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature similarity index 100% rename from fdc3-for-web-implementation/packages/client/test/unused-features/desktop-agent-strategy.feature rename to fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature diff --git a/fdc3-for-web-implementation/packages/client/test/step-definitions/intent-resolver.steps.ts b/fdc3-for-web-implementation/packages/client/test/step-definitions/intent-resolver.steps.ts index da8e15902..651814d8b 100644 --- a/fdc3-for-web-implementation/packages/client/test/step-definitions/intent-resolver.steps.ts +++ b/fdc3-for-web-implementation/packages/client/test/step-definitions/intent-resolver.steps.ts @@ -1,6 +1,8 @@ import { Given, When } from "@cucumber/cucumber"; import { CustomWorld } from "../world"; import { handleResolve } from "@kite9/testing"; +import { DefaultDesktopAgentIntentResolver } from "../../src/ui/DefaultDesktopAgentIntentResolver"; +import { INTENT_RESPOLVER_URL } from "../support/MockFDC3Server"; const contextMap: Record = { @@ -29,6 +31,34 @@ Given('{string} is a {string} context', function (this: CustomWorld, field: stri this.props[field] = contextMap[type]; }) + +Given('An Intent Resolver in {string}', async function (this: CustomWorld, field: string) { + const cs = new DefaultDesktopAgentIntentResolver(INTENT_RESPOLVER_URL); + this.props[field] = cs + await cs.connect() +}) + +Given('{string} is an AppIntents array with a ViewNews intent and two apps', function (this: CustomWorld, field: string) { + this.props[field] = [ + { + intent: { + name: 'ViewNews' + }, + apps: [ + { + appId: 'app1' + }, + { + appId: 'app2' + } + ] + } + + + ] +}) + + When('I call {string} with {string} with parameters {string} and {string} for a promise', function (this: CustomWorld, field: string, fnName: string, param1: string, param2: string) { try { const object = handleResolve(field, this) @@ -42,29 +72,55 @@ When('I call {string} with {string} with parameters {string} and {string} for a } }); -Given('{string} receives a {string} message for the intent resolver and pipes comms to {string}', async function (this: CustomWorld, frame: string, type: string, output: string) { - const channelSelectorIframe = handleResolve(frame, this) - const mc = new MessageChannel(); - const internalPort = mc.port1; - const externalPort = mc.port2; - - if (type == "SelectorMessageInitialize") { - globalThis.window.dispatchEvent({ - type: 'message', - data: { - type: 'SelectorMessageInitialize' +Given('The intent resolver sends an intent selection message', async function (this: CustomWorld) { + const port = handleResolve("{document.iframes[0].messageChannels[0].port2}", this) + + port.postMessage({ + type: 'iframeResolveAction', + payload: { + action: 'click', + appIdentifier: { + appId: 'app1' }, - origin: globalThis.window.location.origin, - ports: [externalPort], - source: channelSelectorIframe - } as any) - } + intent: 'ViewNews' + } + }) +}) - const out: any[] = [] - this.props[output] = out +Given('The intent resolver cancels the intent selection message', async function (this: CustomWorld) { + const port = handleResolve("{document.iframes[0].messageChannels[0].port2}", this) - internalPort.start() - internalPort.onmessage = (e) => { - out.push({ type: e.type, data: e.data }) - } -}); + port.postMessage({ + type: 'iframeResolveAction', + payload: { + action: 'cancel' + } + }) +}) + +// Given('{string} receives a {string} message for the intent resolver and pipes comms to {string}', async function (this: CustomWorld, frame: string, type: string, output: string) { +// const channelSelectorIframe = handleResolve(frame, this) +// const mc = new MessageChannel(); +// const internalPort = mc.port1; +// const externalPort = mc.port2; + +// if (type == "SelectorMessageInitialize") { +// globalThis.window.dispatchEvent({ +// type: 'message', +// data: { +// type: 'SelectorMessageInitialize' +// }, +// origin: globalThis.window.location.origin, +// ports: [externalPort], +// source: channelSelectorIframe +// } as any) +// } + +// const out: any[] = [] +// this.props[output] = out + +// internalPort.start() +// internalPort.onmessage = (e) => { +// out.push({ type: e.type, data: e.data }) +// } +// }); diff --git a/fdc3-for-web-implementation/packages/client/test/unused-features/default-intent-resolver.feature b/fdc3-for-web-implementation/packages/client/test/unused-features/default-intent-resolver.feature deleted file mode 100644 index 1cfd7665f..000000000 --- a/fdc3-for-web-implementation/packages/client/test/unused-features/default-intent-resolver.feature +++ /dev/null @@ -1,25 +0,0 @@ -Feature: Default Intent Resolver - - Background: Desktop Agent API - Given a browser document in "document" and window in "window" - And A Dummy Desktop Agent in "dummy-api" - And "dummyFailover" is a function which returns a promise of "{dummy-api}" - #And Testing ends after "5000" ms - And "instrumentContext" is a "fdc3.instrument" context - - Scenario: App Requests Intent Resolution - Given I call "{dummy-api}" with "raiseIntent" with parameters "viewNews" and "{instrumentContext}" for a promise - And I refer to "{result}" as "theIntentPromise" - And we wait for a period of "200" ms - When "{document.iframes[1]}" receives a "SelectorMessageInitialize" message for the "intentResolver" and creates port "intentResolverPort" - And "{intentResolverPort}" pipes messages to "output" - And we wait for a period of "200" ms - Then "{output}" is an array of objects with the following contents - | type | data.type | data.appIntents[0].intent | data.appIntents[0].apps[0].appId | data.appIntents[0].apps[1].appId | - | message | ResolverIntents | viewNews | test-app-1 | test-app-2 | - When we wait for a period of "200" ms - And "{intentResolverPort}" receives a "ResolverMessageChoice" message - Then the promise "{theIntentPromise}" should resolve - And "{result}" is an object with the following contents - | intent | target | - | viewNews | test-app-1 | From a97dd50d6aa3eff43f7db91591003b760a9f857e Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Thu, 29 Aug 2024 12:57:17 +0100 Subject: [PATCH 10/11] Finished client testing --- .../packages/client/src/index.ts | 31 ++++----- .../src/messaging/AbstractWebMessaging.ts | 2 +- .../src/strategies/PostMessageLoader.ts | 36 +++++++++- .../features/desktop-agent-strategy.feature | 40 ++++++++++- .../step-definitions/desktop-agent.steps.ts | 37 ++++++++-- .../test/support/responses/Handshake.ts | 67 ++++++++++++------- .../src/messaging/AbstractMessaging.ts | 3 +- 7 files changed, 159 insertions(+), 57 deletions(-) diff --git a/fdc3-for-web-implementation/packages/client/src/index.ts b/fdc3-for-web-implementation/packages/client/src/index.ts index 588d33203..28581f49a 100644 --- a/fdc3-for-web-implementation/packages/client/src/index.ts +++ b/fdc3-for-web-implementation/packages/client/src/index.ts @@ -1,7 +1,7 @@ import { DesktopAgent, } from '@finos/fdc3' import { getAgent as getAgentType, GetAgentParams } from '@kite9/fdc3-common'; import { ElectronEventLoader } from './strategies/ElectronEventLoader' -import { PostMessageLoader } from './strategies/PostMessageLoader' +import { handleWindowProxy, PostMessageLoader } from './strategies/PostMessageLoader' import { TimeoutLoader } from './strategies/TimeoutLoader' const DEFAULT_WAIT_FOR_MS = 20000; @@ -59,15 +59,8 @@ export const getAgent: getAgentType = (optionsOverride?: GetAgentParams) => { }) .catch(async (error) => { if (options.failover) { - const o = await options.failover(options) - - if ((o as any).getInfo) { - return o as DesktopAgent - } else { - // todo: turn the window proxy into a desktop agent - return o as DesktopAgent - } - + const o = await handleWindowProxy(options, () => { return options.failover!!(options) }) + return o } else { throw error } @@ -77,15 +70,15 @@ export const getAgent: getAgentType = (optionsOverride?: GetAgentParams) => { /** * Replaces the original fdc3Ready function from FDC3 2.0 with a new one that uses the new getClientAPI function. - * + * * @param waitForMs Amount of time to wait before failing the promise (20 seconds is the default). * @returns A DesktopAgent promise. */ -export function fdc3Ready(waitForMs = DEFAULT_WAIT_FOR_MS): Promise { - return getAgent({ - timeout: waitForMs, - dontSetWindowFdc3: false, - channelSelector: true, - intentResolver: true - }) -} \ No newline at end of file +// export function fdc3Ready(waitForMs = DEFAULT_WAIT_FOR_MS): Promise { +// return getAgent({ +// timeout: waitForMs, +// dontSetWindowFdc3: false, +// channelSelector: true, +// intentResolver: true +// }) +// } \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/src/messaging/AbstractWebMessaging.ts b/fdc3-for-web-implementation/packages/client/src/messaging/AbstractWebMessaging.ts index e66fea0ba..bc20d7494 100644 --- a/fdc3-for-web-implementation/packages/client/src/messaging/AbstractWebMessaging.ts +++ b/fdc3-for-web-implementation/packages/client/src/messaging/AbstractWebMessaging.ts @@ -1,7 +1,7 @@ import { DesktopAgentDetails, WebDesktopAgentType, GetAgentParams, WebConnectionProtocol5ValidateAppIdentitySuccessResponse } from "@kite9/fdc3-common"; import { RegisterableListener, AbstractMessaging } from "@kite9/da-proxy"; -const DESKTOP_AGENT_SESSION_STORAGE_DETAILS_KEY = "fdc3-desktop-agent-details" +export const DESKTOP_AGENT_SESSION_STORAGE_DETAILS_KEY = "fdc3-desktop-agent-details" /** * Version of Messaging which is able to store details in the SessionState (i.e. works on the web) diff --git a/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts b/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts index 791152e00..0f3967beb 100644 --- a/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts +++ b/fdc3-for-web-implementation/packages/client/src/strategies/PostMessageLoader.ts @@ -1,4 +1,4 @@ -import { GetAgentParams, WebConnectionProtocol1Hello, WebConnectionProtocol2LoadURL } from '@kite9/fdc3-common' +import { GetAgentParams, WebConnectionProtocol1Hello, WebConnectionProtocol2LoadURL, WebConnectionProtocol3Handshake } from '@kite9/fdc3-common' import { FDC3_VERSION } from '..'; import { createDesktopAgentAPI } from '../messaging/message-port'; import { v4 as uuidv4 } from "uuid" @@ -58,7 +58,7 @@ function openFrame(url: string): Window { return ifrm.contentWindow!! } -function helloExchange(options: GetAgentParams, connectionAttemptUuid: string): Promise { +export function helloExchange(options: GetAgentParams, connectionAttemptUuid: string): Promise { return new Promise((resolve, _reject) => { // setup listener for message and retrieve JS URL from it @@ -84,6 +84,38 @@ function helloExchange(options: GetAgentParams, connectionAttemptUuid: string): } +/** + * This is a variation of the PostMessageLoader used for handling failover. If the failover returns the WindowProxy this is used + * to properly load the desktop agent. + */ +export function handleWindowProxy(options: GetAgentParams, provider: () => Promise): Promise { + return new Promise((resolve, _reject) => { + const el = (event: MessageEvent) => { + const data = event.data; + if (data.type == 'WCP3Handshake') { + const handshake = data as WebConnectionProtocol3Handshake; + globalThis.window.removeEventListener("message", el) + + resolve(createDesktopAgentAPI({ + connectionAttemptUuid: handshake.meta.connectionAttemptUuid, + handshake: data, + messagePort: event.ports[0], + options: options + })) + } + } + + globalThis.window.addEventListener("message", el) + + provider().then((providerResult) => { + if ((providerResult as any).getInfo) { + globalThis.window.removeEventListener("message", el) + resolve(providerResult as DesktopAgent) + } + }) + }) +} + export class PostMessageLoader implements Loader { diff --git a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature index aefdd874d..17715443d 100644 --- a/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature +++ b/fdc3-for-web-implementation/packages/client/test/features/desktop-agent-strategy.feature @@ -2,7 +2,7 @@ Feature: Different Strategies for Accessing the Desktop Agent Background: Desktop Agent API Given a browser document in "document" and window in "window" - # And Testing ends after "8000" ms + And Testing ends after "8000" ms Scenario: Running inside a Browser and using post message with direct message ports Given Parent Window desktop "da" listens for postMessage events in "{window}", returns direct message response @@ -79,7 +79,7 @@ Feature: Different Strategies for Accessing the Desktop Agent | 2.0 | cucumber-app | cucumber-provider | Then I call "{document}" with "shutdown" - Scenario: Failover Strategy. + Scenario: Failover Strategy returning desktop agent Given A Dummy Desktop Agent in "dummy-api" And "dummyFailover" is a function which returns a promise of "{dummy-api}" And I call getAgentAPI for a promise result with the following options @@ -93,9 +93,45 @@ Feature: Different Strategies for Accessing the Desktop Agent | 2.0 | cucumber-app | cucumber-provider | Then I call "{document}" with "shutdown" + Scenario: Failover Strategy returning a proxy + Given "dummyFailover2" is a function which opens an iframe for communications on "{document}" + And I call getAgentAPI for a promise result with the following options + | failover | timeout | + | {dummyFailover2} | 1000 | + And I refer to "{result}" as "theAPIPromise" + Then the promise "{theAPIPromise}" should resolve + And I call "{result}" with "getInfo" + Then "{result}" is an object with the following contents + | fdc3Version | appMetadata.appId | provider | + | 2.0 | cucumber-app | cucumber-provider | + Then I call "{document}" with "shutdown" + Scenario: Recovery from SessionState Here, we recover the details of the session from the session state, obviating the need to make a request to the parent iframe. + Given Parent Window desktop "da" listens for postMessage events in "{window}", returns direct message response + And an existing app instance in "instanceID" + And the session identity is set to "{instanceID}" + And we wait for a period of "200" ms + And I call getAgentAPI for a promise result with the following options + | dontSetWindowFdc3 | timeout | intentResolver | channelSelector | + | true | 8000 | false | false | + And I refer to "{result}" as "theAPIPromise" + Then the promise "{theAPIPromise}" should resolve + Then I call "{document}" with "shutdown" + And I call "{desktopAgent}" with "disconnect" + Scenario: Failed Recovery from SessionState App tries to recover with an ID that doesn't exist. + + Given Parent Window desktop "da" listens for postMessage events in "{window}", returns direct message response + And we wait for a period of "200" ms + And the session identity is set to "BAD_INSTANCE" + And I call getAgentAPI for a promise result with the following options + | dontSetWindowFdc3 | timeout | intentResolver | channelSelector | + | true | 8000 | false | false | + And I refer to "{result}" as "theAPIPromise" + Then the promise "{theAPIPromise}" should resolve + And "{result}" is an error with message "Invalid instance" + Then I call "{document}" with "shutdown" diff --git a/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts b/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts index 1ab9b4f61..952a6f4bc 100644 --- a/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts +++ b/fdc3-for-web-implementation/packages/client/test/step-definitions/desktop-agent.steps.ts @@ -1,16 +1,13 @@ import { After, DataTable, Given, When } from '@cucumber/cucumber' import { CustomWorld } from '../world'; import { handleResolve, setupGenericSteps } from '@kite9/testing'; -//import { BasicDesktopAgent, DefaultChannelSupport, DefaultHandshakeSupport, DefaultIntentSupport } from '@kite9/da-proxy'; import { MockDocument, MockWindow } from '../support/MockDocument'; import { getAgent } from '../../src'; -import { GetAgentParams } from '@kite9/fdc3-common'; -import { dummyInstanceId, MockFDC3Server } from '../support/MockFDC3Server'; -// import { DefaultDesktopAgentIntentResolver } from '../../src/ui/DefaultDesktopAgentIntentResolver'; -// import { DefaultDesktopAgentChannelSelector } from '../../src/ui/DefaultDesktopAgentChannelSelector'; -// import { NoopAppSupport } from '../../src/apps/NoopAppSupport'; +import { DesktopAgentDetails, GetAgentParams, WebDesktopAgentType } from '@kite9/fdc3-common'; +import { dummyInstanceId, EMBED_URL, MockFDC3Server } from '../support/MockFDC3Server'; import { MockStorage } from '../support/MockStorage'; import { DesktopAgent, ImplementationMetadata } from '@finos/fdc3'; +import { DESKTOP_AGENT_SESSION_STORAGE_DETAILS_KEY } from '../../src/messaging/AbstractWebMessaging'; var wtf = require('wtfnode') setupGenericSteps() @@ -28,6 +25,23 @@ Given('Parent Window desktop {string} listens for postMessage events in {string} this.mockContext.open(dummyInstanceId.appId) }) +Given('{string} is a function which opens an iframe for communications on {string}', function (this: CustomWorld, fn: string, doc: string) { + this.props[fn] = () => { + this.mockContext.open(dummyInstanceId.appId) + const document = handleResolve(doc, this) as MockDocument + var ifrm = document.createElement("iframe") + this.mockFDC3Server = new MockFDC3Server(ifrm as any, false, this.mockContext) + ifrm.setAttribute("src", EMBED_URL + "?connectionAttemptUuid=123") + document.body.appendChild(ifrm) + return ifrm + } +}); + +Given('an existing app instance in {string}', async function (this: CustomWorld, field: string) { + const uuid = this.mockContext.open(dummyInstanceId.appId) + this.props[field] = uuid +}) + Given('A Dummy Desktop Agent in {string}', async function (this: CustomWorld, field: string) { @@ -102,4 +116,15 @@ Given('a browser document in {string} and window in {string}', async function (t // browser storage globalThis.sessionStorage = new MockStorage() as any +}) + +Given("the session identity is set to {string}", async function (this: CustomWorld, id: string) { + const details: DesktopAgentDetails = { + agentType: WebDesktopAgentType.PROXY_PARENT, + instanceUuid: handleResolve(id, this), + appId: 'cucumber-app', + instanceId: 'uuid-0' + } + + globalThis.sessionStorage.setItem(DESKTOP_AGENT_SESSION_STORAGE_DETAILS_KEY, JSON.stringify(details)) }) \ No newline at end of file diff --git a/fdc3-for-web-implementation/packages/client/test/support/responses/Handshake.ts b/fdc3-for-web-implementation/packages/client/test/support/responses/Handshake.ts index d422035be..2a27e6315 100644 --- a/fdc3-for-web-implementation/packages/client/test/support/responses/Handshake.ts +++ b/fdc3-for-web-implementation/packages/client/test/support/responses/Handshake.ts @@ -2,7 +2,9 @@ import { TestServerContext } from "../TestServerContext"; import { InstanceID } from "@kite9/da-server"; import { AutomaticResponse } from "./AutomaticResponses"; -import { WebConnectionProtocol4ValidateAppIdentity, WebConnectionProtocol5ValidateAppIdentitySuccessResponse } from "@kite9/fdc3-common"; +import { WebConnectionProtocol4ValidateAppIdentity, WebConnectionProtocol5ValidateAppIdentityFailedResponse, WebConnectionProtocol5ValidateAppIdentitySuccessResponse } from "@kite9/fdc3-common"; + +export const BAD_INSTANCE_ID = "BAD_INSTANCE" export class Handshake implements AutomaticResponse { @@ -17,31 +19,46 @@ export class Handshake implements AutomaticResponse { return Promise.resolve() } - private createResponse(i: WebConnectionProtocol4ValidateAppIdentity): WebConnectionProtocol5ValidateAppIdentitySuccessResponse { - return { - meta: { - connectionAttemptUuid: i.meta.connectionAttemptUuid, - timestamp: new Date(), - }, - type: "WCP5ValidateAppIdentityResponse", - payload: { - implementationMetadata: { - appMetadata: { - appId: "cucumber-app", - instanceId: "cucumber-instance", - }, - fdc3Version: "2.0", - optionalFeatures: { - DesktopAgentBridging: false, - OriginatingAppMetadata: true, - UserChannelMembershipAPIs: true - }, - provider: "cucumber-provider", - providerVersion: "test" + private createResponse(i: WebConnectionProtocol4ValidateAppIdentity): + WebConnectionProtocol5ValidateAppIdentitySuccessResponse | + WebConnectionProtocol5ValidateAppIdentityFailedResponse { + if (i.payload.instanceUuid == BAD_INSTANCE_ID) { + return { + meta: { + connectionAttemptUuid: i.meta.connectionAttemptUuid, + timestamp: new Date(), }, - appId: 'cucumber-app', - instanceId: 'cucumber-instance', - instanceUuid: 'some-instance-uuid', + type: "WCP5ValidateAppIdentityFailedResponse", + payload: { + message: "Invalid instance" + } + } + } else { + return { + meta: { + connectionAttemptUuid: i.meta.connectionAttemptUuid, + timestamp: new Date(), + }, + type: "WCP5ValidateAppIdentityResponse", + payload: { + implementationMetadata: { + appMetadata: { + appId: "cucumber-app", + instanceId: "cucumber-instance", + }, + fdc3Version: "2.0", + optionalFeatures: { + DesktopAgentBridging: false, + OriginatingAppMetadata: true, + UserChannelMembershipAPIs: true + }, + provider: "cucumber-provider", + providerVersion: "test" + }, + appId: 'cucumber-app', + instanceId: 'cucumber-instance', + instanceUuid: 'some-instance-uuid', + } } } } diff --git a/fdc3-for-web-implementation/packages/da-proxy/src/messaging/AbstractMessaging.ts b/fdc3-for-web-implementation/packages/da-proxy/src/messaging/AbstractMessaging.ts index acb05e341..2c27089b7 100644 --- a/fdc3-for-web-implementation/packages/da-proxy/src/messaging/AbstractMessaging.ts +++ b/fdc3-for-web-implementation/packages/da-proxy/src/messaging/AbstractMessaging.ts @@ -111,8 +111,7 @@ export abstract class AbstractMessaging implements Messaging { private async exchangeValidationWithId(message: any, connectionAttemptUuid: string): Promise { const prom = this.waitFor(m => - (m.meta.connectionAttemptUuid == connectionAttemptUuid) - && (m.meta.requestUuid == message.meta.requestUuid)) + (m.meta.connectionAttemptUuid == connectionAttemptUuid)) this.post(message) const out: any = await prom if (out?.payload?.message) { From a19bb558ee9ee24c6c9ce38dcc8b3e4089a49530 Mon Sep 17 00:00:00 2001 From: Rob Moffat Date: Thu, 29 Aug 2024 13:20:24 +0100 Subject: [PATCH 11/11] Updated package lock --- fdc3-for-web-implementation/package-lock.json | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/fdc3-for-web-implementation/package-lock.json b/fdc3-for-web-implementation/package-lock.json index 5dbf9405b..a6b54e4a6 100644 --- a/fdc3-for-web-implementation/package-lock.json +++ b/fdc3-for-web-implementation/package-lock.json @@ -1938,6 +1938,11 @@ "@types/node": "*" } }, + "node_modules/@types/wtfnode": { + "version": "0.7.3", + "integrity": "sha512-UMkHpx+o2xRWLJ7PmT3bBzvIA9/0oFw80oPtY/xO4jfdq+Gznn4wP7K9B/JjMxyxy+wF+5oRPIykxeBbEDjwRg==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.33", "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", @@ -9350,6 +9355,14 @@ } } }, + "node_modules/wtfnode": { + "version": "0.9.3", + "integrity": "sha512-MXjgxJovNVYUkD85JBZTKT5S5ng/e56sNuRZlid7HcGTNrIODa5UPtqE3i0daj7fJ2SGj5Um2VmiphQVyVKK5A==", + "dev": true, + "bin": { + "wtfnode": "proxy.js" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", @@ -9446,6 +9459,7 @@ } }, "packages/client": { + "name": "@kite9/client", "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", @@ -9458,15 +9472,18 @@ "@cucumber/cucumber": "10.3.1", "@kite9/da-server": "0.0.54", "@types/node": "^20.14.11", + "@types/wtfnode": "^0.7.3", "expect": "^29.7.0", "jsonpath-plus": "^9.0.0", "nyc": "15.1.0", "rimraf": "^6.0.1", "tsx": "^4.7.1", - "typescript": "^5.3.2" + "typescript": "^5.3.2", + "wtfnode": "^0.9.3" } }, "packages/da-proxy": { + "name": "@kite9/da-proxy", "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", @@ -9501,6 +9518,7 @@ } }, "packages/da-server": { + "name": "@kite9/da-server", "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", @@ -9537,6 +9555,7 @@ } }, "packages/demo": { + "name": "@kite9/demo", "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6", @@ -9562,6 +9581,7 @@ } }, "packages/fdc3-common": { + "name": "@kite9/fdc3-common", "version": "0.0.54", "dependencies": { "@finos/fdc3": "^2.1.0-beta.6" @@ -9572,6 +9592,7 @@ } }, "packages/testing": { + "name": "@kite9/testing", "version": "0.0.54", "dependencies": { "@cucumber/cucumber": "10.3.1",