diff --git a/src/plugin/__tests__/settings.test.ts b/src/plugin/__tests__/settings.test.ts index 98a46eb..38d3492 100644 --- a/src/plugin/__tests__/settings.test.ts +++ b/src/plugin/__tests__/settings.test.ts @@ -76,25 +76,66 @@ describe("settings", () => { } satisfies Settings); }); - /** - * Asserts {@link setGlobalSettings} sends the command to the {@link connection}. - */ - it("setGlobalSettings", async () => { - // Arrange, act. - await setGlobalSettings({ - name: "Elgato", - }); + describe("setGlobalSettings", () => { - // Assert. - expect(connection.send).toHaveBeenCalledTimes(1); - expect(connection.send).toHaveBeenCalledWith<[SetGlobalSettings]>({ - event: "setGlobalSettings", - context: connection.registrationParameters.pluginUUID, - payload: { + /** + * Asserts {@link setGlobalSettings} sends the command to the {@link connection}. + */ + it("with object", async () => { + // Arrange, act. + await setGlobalSettings({ name: "Elgato", - }, + }); + + // Assert. + expect(connection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledWith<[SetGlobalSettings]>({ + event: "setGlobalSettings", + context: connection.registrationParameters.pluginUUID, + payload: { + name: "Elgato", + }, + }); }); - }); + + /** + * Asserts {@link setGlobalSettings} invokes the provided callback with the current settings + * object and passes the return value via send command using {@link connection}. + */ + it("with callback", async () => { + + // Arrange + + const current = { + name: "Current" + } + const didReceiveGlobalSettingsEvent: DidReceiveGlobalSettings = { + event: "didReceiveGlobalSettings", + payload: { + settings: current, + }, + }; + const expected = { + name: "Changed", + } + const callback = vi.fn(() => expected); + + // Act + + const execution = setGlobalSettings(callback); + connection.emit("didReceiveGlobalSettings", didReceiveGlobalSettingsEvent); + await execution; + + // Assert + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(current); + expect(connection.send).toHaveBeenCalledWith( + expect.objectContaining({ + payload: expected, + })); + }); + }) }); describe("receiving emits with useExperimentalMessageIdentifiers set to false", () => { diff --git a/src/plugin/actions/__tests__/action.test.ts b/src/plugin/actions/__tests__/action.test.ts index 412f7a8..102317a 100644 --- a/src/plugin/actions/__tests__/action.test.ts +++ b/src/plugin/actions/__tests__/action.test.ts @@ -1,7 +1,7 @@ import { beforeAll, describe, expect, it, test, vi } from "vitest"; import type { Settings } from "../../../api/__mocks__/events.js"; -import { DeviceType, type GetSettings, type SetSettings, type ShowAlert, type WillAppear } from "../../../api/index.js"; +import { DeviceType, type DidReceiveSettings, type GetSettings, type SetSettings, type ShowAlert, type WillAppear } from "../../../api/index.js"; import type { JsonObject } from "../../../common/json.js"; import { connection } from "../../connection.js"; import { Device } from "../../devices/device.js"; @@ -169,26 +169,78 @@ describe("Action", () => { let action!: Action; beforeAll(() => (action = new Action(source))); - /** - * Asserts {@link Action.setSettings} forwards the command to the {@link connection}. - */ - it("setSettings", async () => { - // Arrange, act. - await action.setSettings({ - name: "Elgato", - }); + describe("setSettings", async () => { - // Assert. - expect(connection.send).toHaveBeenCalledTimes(1); - expect(connection.send).toHaveBeenCalledWith<[SetSettings]>({ - context: action.id, - event: "setSettings", - payload: { + /** + * Asserts {@link Action.setSettings} forwards the command to the {@link connection}. + */ + it("with object", async () => { + // Arrange, act. + await action.setSettings({ name: "Elgato", - }, + }); + + // Assert. + expect(connection.send).toHaveBeenCalledTimes(1); + expect(connection.send).toHaveBeenCalledWith<[SetSettings]>({ + context: action.id, + event: "setSettings", + payload: { + name: "Elgato", + }, + }); + }); + + it("with callback", async () => { + + // Arrange + + const current = { + name: "Current", + } + const expected = { + name: "Changed", + }; + + const callback = vi.fn(() => expected); + + const didReceiveSettingsEvent: DidReceiveSettings = { + action: "any.acton", + context: action.id, + event: "didReceiveSettings", + device: "any", + payload: { + controller: "Keypad", + coordinates: { + column: 0, + row: 0, + }, + isInMultiAction: false, + resources: {}, + settings: current, + }, + }; + + // Act + + const execution = action.setSettings(callback); + + connection.emit("didReceiveSettings", didReceiveSettingsEvent); + + await execution; + + // Assert + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(current); + expect(connection.send).toHaveBeenCalledWith( + expect.objectContaining({ + payload: expected, + })); }); }); + /** * Asserts {@link Action.showAlert} forwards the command to the {@link connection}. */ diff --git a/src/plugin/actions/action.ts b/src/plugin/actions/action.ts index 63fd821..fbc06da 100644 --- a/src/plugin/actions/action.ts +++ b/src/plugin/actions/action.ts @@ -86,15 +86,25 @@ export class Action extends ActionContext { } /** - * Sets the {@link settings} associated with this action instance. Use in conjunction with {@link Action.getSettings}. - * @param settings Settings to persist. + * Sets the {@link settings} associated with this action instance. See also {@link Action.getSettings}. + * + * In addition to passing a settings object directly, this method also supports a functional + * updater. When a function is provided, it will be invoked with the current settings and must + * return the updated settings, either synchronously or asynchronously. + * @param settings Either a settings object to persist, or a function that receives the current + * settings and returns the new settings (or a Promise resolving to them). * @returns `Promise` resolved when the {@link settings} are sent to Stream Deck. */ - public setSettings(settings: U): Promise { + public async setSettings(settings: U | ((settings: U) => Promise | U)): Promise { + + const payload = typeof settings === "function" + ? await settings(await this.getSettings()) + : settings; + return connection.send({ event: "setSettings", context: this.id, - payload: settings, + payload, }); } diff --git a/src/plugin/settings.ts b/src/plugin/settings.ts index 1fbd45f..6ecdea1 100644 --- a/src/plugin/settings.ts +++ b/src/plugin/settings.ts @@ -10,6 +10,17 @@ import { requiresVersion } from "./validation.js"; let __useExperimentalMessageIdentifiers = false; +const getGlobalSettings = (): Promise => { + return new Promise((resolve) => { + connection.once("didReceiveGlobalSettings", (ev: DidReceiveGlobalSettings) => resolve(ev.payload.settings)); + connection.send({ + event: "getGlobalSettings", + context: connection.registrationParameters.pluginUUID, + id: randomUUID(), + }); + }); +}; + export const settings = { /** * Available from Stream Deck 7.1; determines whether message identifiers should be sent when getting @@ -40,16 +51,7 @@ export const settings = { * @template T The type of global settings associated with the plugin. * @returns Promise containing the plugin's global settings. */ - getGlobalSettings: (): Promise => { - return new Promise((resolve) => { - connection.once("didReceiveGlobalSettings", (ev: DidReceiveGlobalSettings) => resolve(ev.payload.settings)); - connection.send({ - event: "getGlobalSettings", - context: connection.registrationParameters.pluginUUID, - id: randomUUID(), - }); - }); - }, + getGlobalSettings, /** * Occurs when the global settings are requested, or when the the global settings were updated in @@ -104,11 +106,16 @@ export const settings = { * connectedDate: new Date() * }) */ - setGlobalSettings: async (settings: T): Promise => { + setGlobalSettings: async (settings: T | ((current: T) => Promise | T)): Promise => { + + const payload = typeof settings === "function" + ? await settings(await getGlobalSettings()) + : settings; + await connection.send({ event: "setGlobalSettings", context: connection.registrationParameters.pluginUUID, - payload: settings, + payload, }); }, };