diff --git a/src/renderer/application/useCases/widget/getWidgetApi.ts b/src/renderer/application/useCases/widget/getWidgetApi.ts index 24047c1..7e697cc 100644 --- a/src/renderer/application/useCases/widget/getWidgetApi.ts +++ b/src/renderer/application/useCases/widget/getWidgetApi.ts @@ -8,9 +8,10 @@ import { ProcessProvider } from '@/application/interfaces/processProvider'; import { ShellProvider } from '@/application/interfaces/shellProvider'; import { DataStorageRenderer } from '@/application/interfaces/dataStorage'; import { EntityId } from '@/base/entity'; -import { WidgetApiModuleName, WidgetApiSetContextMenuFactoryHandler, WidgetApiUpdateActionBarHandler, createWidgetApiFactory } from '@/base/widgetApi'; +import { WidgetApiExposeApiHandler, WidgetApiModuleName, WidgetApiSetContextMenuFactoryHandler, WidgetApiUpdateActionBarHandler, createWidgetApiFactory } from '@/base/widgetApi'; import { ObjectManager } from '@common/base/objectManager'; import { TerminalProvider } from '@/application/interfaces/terminalProvider'; +import { GetWidgetsInCurrentWorkflowUseCase } from '@/application/useCases/widget/widgetApiWidgets/getWidgetsInCurrentWorkflow'; interface Deps { clipboardProvider: ClipboardProvider; @@ -18,6 +19,7 @@ interface Deps { processProvider: ProcessProvider; shellProvider: ShellProvider; terminalProvider: TerminalProvider; + getWidgetsInCurrentWorkflowUseCase: GetWidgetsInCurrentWorkflowUseCase; } function _createWidgetApiFactory({ clipboardProvider, @@ -25,15 +27,19 @@ function _createWidgetApiFactory({ shellProvider, widgetDataStorageManager, terminalProvider, + getWidgetsInCurrentWorkflowUseCase, }: Deps, forPreview: boolean) { return createWidgetApiFactory( - (_widgetId, updateActionBarHandler, setWidgetContextMenuFactoryHandler) => ({ + (_widgetId, updateActionBarHandler, setWidgetContextMenuFactoryHandler, exposeApiHandler) => ({ updateActionBar: !forPreview ? (actionBarItems) => { updateActionBarHandler(actionBarItems); } : () => undefined, setContextMenuFactory: !forPreview ? (factory) => { setWidgetContextMenuFactoryHandler(factory); - } : () => undefined + } : () => undefined, + exposeApi: !forPreview ? (api) => { + exposeApiHandler(api) + } : () => undefined, }), { clipboard: () => ({ @@ -63,6 +69,9 @@ function _createWidgetApiFactory({ terminal: () => ({ execCmdLines: (cmdLines, cwd) => terminalProvider.execCmdLines(cmdLines, cwd) }), + widgets: () => ({ + getWidgetsInCurrentWorkflow: (widgetTypeId) => getWidgetsInCurrentWorkflowUseCase(widgetTypeId) + }) } ) } @@ -76,11 +85,12 @@ export function createGetWidgetApiUseCase(deps: Deps) { forPreview: boolean, updateActionBarHandler: WidgetApiUpdateActionBarHandler, setContextMenuFactoryHandler: WidgetApiSetContextMenuFactoryHandler, + exposeApiHandler: WidgetApiExposeApiHandler, requiredModules: WidgetApiModuleName[] ) { return forPreview - ? widgetApiPreviewFactory(widgetId, updateActionBarHandler, setContextMenuFactoryHandler, requiredModules) - : widgetApiFactory(widgetId, updateActionBarHandler, setContextMenuFactoryHandler, requiredModules); + ? widgetApiPreviewFactory(widgetId, updateActionBarHandler, setContextMenuFactoryHandler, exposeApiHandler, requiredModules) + : widgetApiFactory(widgetId, updateActionBarHandler, setContextMenuFactoryHandler, exposeApiHandler, requiredModules); } return getWidgetApiUseCase; diff --git a/src/renderer/application/useCases/widget/setExposedApi.ts b/src/renderer/application/useCases/widget/setExposedApi.ts new file mode 100644 index 0000000..c02eae2 --- /dev/null +++ b/src/renderer/application/useCases/widget/setExposedApi.ts @@ -0,0 +1,40 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { AppStore } from '@/application/interfaces/store'; +import { EntityId } from '@/base/entity'; +import { updateOneInEntityCollection } from '@/base/entityCollection'; + +interface Deps { + appStore: AppStore, +} + +export function createSetExposedApiUseCase({ + appStore, +}: Deps) { + return function setExposedApiUseCase( + widgetId: EntityId, + api: object + ) { + const state = appStore.get(); + const newWidgets = updateOneInEntityCollection(state.entities.widgets, { + changes: { + exposedApi: api + }, + id: widgetId + }) + if (newWidgets !== state.entities.widgets) { + appStore.set({ + ...state, + entities: { + ...state.entities, + widgets: newWidgets + } + }) + } + } +} + +export type SetExposedApiUseCase = ReturnType; diff --git a/src/renderer/application/useCases/widget/widgetApiWidgets/getWidgetsInCurrentWorkflow.ts b/src/renderer/application/useCases/widget/widgetApiWidgets/getWidgetsInCurrentWorkflow.ts new file mode 100644 index 0000000..2ebc10e --- /dev/null +++ b/src/renderer/application/useCases/widget/widgetApiWidgets/getWidgetsInCurrentWorkflow.ts @@ -0,0 +1,51 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { AppStore } from '@/application/interfaces/store'; +import { getOneFromEntityCollection } from '@/base/entityCollection'; +import { mapIdListToEntityList } from '@/base/entityList'; +import { WidgetApiWidget } from '@/base/widgetApi'; + +interface Deps { + appStore: AppStore, +} + +export function createGetWidgetsInCurrentWorkflowUseCase({ + appStore, +}: Deps) { + return function getWidgetsInCurrentWorkflowUseCase( + widgetTypeId: string, + ): WidgetApiWidget[] { + const state = appStore.get(); + const curPrjId = state.ui.projectSwitcher.currentProjectId; + const curWflId = state.entities.projects[curPrjId]?.currentWorkflowId; + if (!curWflId) { + return []; + } + const curWfl = getOneFromEntityCollection(state.entities.workflows, curWflId); + if (!curWfl) { + return []; + } + + const wgtType = getOneFromEntityCollection(state.entities.widgetTypes, widgetTypeId); + if (!wgtType) { + return []; + } + + return mapIdListToEntityList(state.entities.widgets, curWfl.layout.map(item => item.widgetId)) + .filter(({ type }) => widgetTypeId === type) + .map(({ id, coreSettings, exposedApi }) => { + const { name } = coreSettings; + const api = exposedApi || {}; + return { + id, + name, + api: api as T + } + }) + } +} + +export type GetWidgetsInCurrentWorkflowUseCase = ReturnType; diff --git a/src/renderer/base/state/app.ts b/src/renderer/base/state/app.ts index 90f116d..6eeefaf 100644 --- a/src/renderer/base/state/app.ts +++ b/src/renderer/base/state/app.ts @@ -46,16 +46,15 @@ export function initAppStateWidgets(appState: AppState): AppState { export function createPersistentAppState(appState: AppState) { const { copy, dragDrop, editMode, palette, memSaver, modalScreens, worktable, ...persistentUi } = appState.ui - const { widgetTypes, /* widgets, */...persistentEntities } = appState.entities; + const { widgetTypes, widgets, ...persistentEntities } = appState.entities; return { - // entities: { - // ...persistentEntities, - // widgets: mapEntityCollection(widgets, widget => { - // const { ...persistentWidget } = widget; - // return persistentWidget - // }) - // }, - entities: persistentEntities, + entities: { + ...persistentEntities, + widgets: mapEntityCollection(widgets, widget => { + const { exposedApi, ...persistentWidget } = widget; + return persistentWidget + }) + }, ui: persistentUi } } diff --git a/src/renderer/base/widget.ts b/src/renderer/base/widget.ts index 1be594d..df33892 100644 --- a/src/renderer/base/widget.ts +++ b/src/renderer/base/widget.ts @@ -28,6 +28,7 @@ export interface Widget extends Entity { readonly type: string; readonly coreSettings: WidgetCoreSettings; readonly settings: TSettings; + readonly exposedApi?: object; } interface WidgetEnvCommon { diff --git a/src/renderer/base/widgetApi.ts b/src/renderer/base/widgetApi.ts index 9e02096..366dc60 100644 --- a/src/renderer/base/widgetApi.ts +++ b/src/renderer/base/widgetApi.ts @@ -12,8 +12,15 @@ import { OpenDialogResult, OpenDirDialogConfig, OpenFileDialogConfig } from '@co interface WidgetApiCommon { readonly updateActionBar: (actionBarItems: ActionBarItems) => void; readonly setContextMenuFactory: (factory: WidgetContextMenuFactory) => void; + readonly exposeApi: (api: T) => void; // exposes api for consumption by other widgets via WidgetAPI.widgets } +// Widget things available for use by other widgets via WidgetAPI.widgets +export interface WidgetApiWidget { + id: EntityId; + name: string; + api: Partial; // api exposed by widget +} interface WidgetApiModules { readonly clipboard: { writeBookmark: (title: string, url: string) => Promise; @@ -39,6 +46,9 @@ interface WidgetApiModules { readonly terminal: { execCmdLines: (cmdLines: ReadonlyArray, cwd?: string) => void; } + readonly widgets: { + getWidgetsInCurrentWorkflow(widgetTypeId: string): ReadonlyArray>; + } } export type WidgetApiModuleName = keyof WidgetApiModules; @@ -47,10 +57,12 @@ export interface WidgetApi extends WidgetApiCommon, WidgetApiModules { } export type WidgetApiUpdateActionBarHandler = (actionBarItems: ActionBarItems) => void; export type WidgetApiSetContextMenuFactoryHandler = (factory: WidgetContextMenuFactory) => void; +export type WidgetApiExposeApiHandler = (api: object) => void; export type WidgetApiCommonFactory = ( widgetId: EntityId, updateActionBarHandler: WidgetApiUpdateActionBarHandler, setContextMenuFactoryHandler: WidgetApiSetContextMenuFactoryHandler, + exposeApiHandler: WidgetApiExposeApiHandler ) => WidgetApiCommon; type WidgetApiModuleFactory = (widgetId: EntityId) => WidgetApiModules[N]; export type WidgetApiModuleFactories = { @@ -61,6 +73,7 @@ export type WidgetApiFactory = ( widgetId: EntityId, updateActionBarHandler: WidgetApiUpdateActionBarHandler, setContextMenuFactoryHandler: WidgetApiSetContextMenuFactoryHandler, + exposeApiHandler: WidgetApiExposeApiHandler, availableModules: WidgetApiModuleName[] ) => WidgetApi; @@ -69,9 +82,10 @@ export function createWidgetApiFactory(commonFactory: WidgetApiCommonFactory, mo widgetId: EntityId, updateActionBarHandler: WidgetApiUpdateActionBarHandler, setContextMenuFactoryHandler: WidgetApiSetContextMenuFactoryHandler, + exposeApiHandler: WidgetApiExposeApiHandler, availableModules: WidgetApiModuleName[] ) => ({ - ...commonFactory(widgetId, updateActionBarHandler, setContextMenuFactoryHandler), + ...commonFactory(widgetId, updateActionBarHandler, setContextMenuFactoryHandler, exposeApiHandler), ...Object.fromEntries(availableModules.map(featName => ([featName, moduleFactories[featName](widgetId)]))) } as WidgetApi); } diff --git a/src/renderer/base/widgetType.ts b/src/renderer/base/widgetType.ts index 8a2e72e..8b680c3 100644 --- a/src/renderer/base/widgetType.ts +++ b/src/renderer/base/widgetType.ts @@ -28,6 +28,6 @@ export interface WidgetType extends Entity { readonly widgetComp: WidgetTypeComponent; readonly settingsEditorComp: WidgetTypeComponent; readonly createSettingsState: CreateSettingsState; - readonly requiresApi?: WidgetApiModuleName[]; + readonly requiresApi?: WidgetApiModuleName[]; // specifies WidgetAPI modules it requires access to readonly requiresState?: SharedStateSliceName[]; } diff --git a/src/renderer/init.ts b/src/renderer/init.ts index 7666afa..7dd26b5 100644 --- a/src/renderer/init.ts +++ b/src/renderer/init.ts @@ -143,6 +143,8 @@ import { createDeactivateWorkflowUseCase } from '@/application/useCases/memSaver import { createToggleTopBarUseCase } from '@/application/useCases/toggleTopBar'; import { createSetProjectSwitcherPositionUseCase } from '@/application/useCases/projectSwitcher/setProjectSwitcherPosition'; import { createSetEditTogglePositionUseCase } from '@/application/useCases/setEditTogglePosition'; +import { createGetWidgetsInCurrentWorkflowUseCase } from '@/application/useCases/widget/widgetApiWidgets/getWidgetsInCurrentWorkflow'; +import { createSetExposedApiUseCase } from '@/application/useCases/widget/setExposedApi'; function prepareDataStorageForRenderer(dataStorage: DataStorage): DataStorageRenderer { return setTextOnlyIfChanged(withJson(dataStorage)); @@ -246,6 +248,8 @@ async function createUseCases(store: ReturnType) { dialog: osDialogProvider }) + const getWidgetsInCurrentWorkflowUseCase = createGetWidgetsInCurrentWorkflowUseCase(deps); + const clipboardProvider = createClipboardProvider(); const shellProvider = createShellProvider(); const processProvider = await createProcessProvider(); @@ -260,6 +264,7 @@ async function createUseCases(store: ReturnType) { shellProvider, widgetDataStorageManager, terminalProvider, + getWidgetsInCurrentWorkflowUseCase, }) const deleteWidgetUseCase = createDeleteWidgetUseCase({ ...deps, @@ -427,6 +432,8 @@ async function createUseCases(store: ReturnType) { const dragWorkflowFromWorkflowSwitcherUseCase = createDragWorkflowFromWorkflowSwitcherUseCase(deps); const dropOnWorkflowSwitcherUseCase = createDropOnWorkflowSwitcherUseCase(deps); + const setExposedApiUseCase = createSetExposedApiUseCase(deps); + return { dragWidgetFromWorktableLayoutUseCase, dragOverWorktableLayoutUseCase, @@ -519,6 +526,8 @@ async function createUseCases(store: ReturnType) { deactivateWorkflowUseCase, initMemSaverUseCase, + + setExposedApiUseCase } } diff --git a/src/renderer/ui/components/widget/widgetViewModel.ts b/src/renderer/ui/components/widget/widgetViewModel.ts index 4537b57..a160144 100644 --- a/src/renderer/ui/components/widget/widgetViewModel.ts +++ b/src/renderer/ui/components/widget/widgetViewModel.ts @@ -19,6 +19,7 @@ import { EntityId } from '@/base/entity'; import { CopyWidgetUseCase } from '@/application/useCases/widget/copyWidget'; import { ShowContextMenuUseCase } from '@/application/useCases/contextMenu/showContextMenu'; import { createSharedState } from '@/base/state/shared'; +import { SetExposedApiUseCase } from '@/application/useCases/widget/setExposedApi'; type Deps = { useAppState: UseAppState; @@ -28,6 +29,7 @@ type Deps = { openWidgetSettingsUseCase: OpenWidgetSettingsUseCase; deleteWidgetUseCase: DeleteWidgetUseCase; copyWidgetUseCase: CopyWidgetUseCase; + setExposedApiUseCase: SetExposedApiUseCase; } export interface WidgetProps { @@ -56,6 +58,7 @@ export function createWidgetViewModelHook({ openWidgetSettingsUseCase, deleteWidgetUseCase, copyWidgetUseCase, + setExposedApiUseCase, }: Deps) { function showMoreActions( id: EntityId, @@ -156,6 +159,7 @@ export function createWidgetViewModelHook({ !!env.isPreview, (items) => setActionBarItemsViewMode([...items, ...createActionBarCommonItemsViewMode(widgetType?.maximizable || false, maximizeAction)]), (factory: WidgetContextMenuFactory | undefined) => setContextMenuFactoryViewMode(() => factory), + (api) => setExposedApiUseCase(widget.id, api), widgetType?.requiresApi || [] ), [env.isPreview, maximizeAction, widget.id, widgetType?.maximizable, widgetType?.requiresApi]) diff --git a/src/renderer/widgets/appModules.ts b/src/renderer/widgets/appModules.ts index ba69f3d..6fa0d88 100644 --- a/src/renderer/widgets/appModules.ts +++ b/src/renderer/widgets/appModules.ts @@ -4,7 +4,7 @@ */ export type { OpenDialogResult } from '@common/base/dialog'; -export type { WidgetApi, WidgetSettingsApi } from '@/base/widgetApi'; +export type { WidgetApi, WidgetSettingsApi, WidgetApiWidget } from '@/base/widgetApi'; export type { WidgetType, CreateSettingsState } from '@/base/widgetType'; export type { WidgetMenuItem, WidgetMenuItemRole, WidgetMenuItems, WidgetContextMenuFactory, WidgetEnv, WidgetSettings } from '@/base/widget'; export type { ActionBarItem, ActionBarItems } from '@/base/actionBar'; diff --git a/src/renderer/widgets/interfaces.ts b/src/renderer/widgets/interfaces.ts new file mode 100644 index 0000000..2c668f4 --- /dev/null +++ b/src/renderer/widgets/interfaces.ts @@ -0,0 +1,9 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +export interface WebpageExposedApi { + openUrl: (url: string) => void; + getUrl: () => string; +} diff --git a/src/renderer/widgets/web-query/index.ts b/src/renderer/widgets/web-query/index.ts index c10f084..f9d27ab 100644 --- a/src/renderer/widgets/web-query/index.ts +++ b/src/renderer/widgets/web-query/index.ts @@ -20,7 +20,7 @@ const widgetType: WidgetType = { createSettingsState, settingsEditorComp, widgetComp, - requiresApi: ['shell'] + requiresApi: ['shell', 'widgets'] } export default widgetType; diff --git a/src/renderer/widgets/web-query/settings.tsx b/src/renderer/widgets/web-query/settings.tsx index bd0f54d..aad92e2 100644 --- a/src/renderer/widgets/web-query/settings.tsx +++ b/src/renderer/widgets/web-query/settings.tsx @@ -5,6 +5,20 @@ import { CreateSettingsState, ReactComponent, SettingsEditorReactComponentProps, SettingBlock } from '@/widgets/appModules'; +export enum SettingsMode { + Browser = 1, + Webpages = 2, +} +const settingsModes = [SettingsMode.Browser, SettingsMode.Webpages] as const; +function isSettingsMode(val: unknown): val is SettingsMode { + if (settingsModes.indexOf(val as SettingsMode)>-1) { + return true; + } + + return false; +} + + export interface SettingsEngine { id: string; name: string; @@ -41,6 +55,7 @@ export const defaultEngine = engineDdgo; export const enginesById = Object.fromEntries(engines.map(item => [item.id, item])); export interface Settings { + mode: SettingsMode; engine: string; descr: string; query: string; @@ -48,30 +63,41 @@ export interface Settings { } export const createSettingsState: CreateSettingsState = (settings) => { - let engineObj: SettingsEngine | undefined; - if (typeof settings.engine === 'string') { - if (settings.engine !== '') { - engineObj = enginesById[settings.engine] - if (!engineObj) { - engineObj = defaultEngine; - } - } - } else { - engineObj = defaultEngine; - } - let engine: string; + const mode = isSettingsMode(settings.mode) ? settings.mode : SettingsMode.Browser; let descr: string; let url: string; - if (engineObj) { - engine = engineObj.id; - descr = ''; - url = ''; + let engine: string; + + if(mode === SettingsMode.Browser) { + let engineObj: SettingsEngine | undefined; + if (typeof settings.engine === 'string') { + if (settings.engine !== '') { + engineObj = enginesById[settings.engine] + if (!engineObj) { + engineObj = defaultEngine; + } + } + } else { + engineObj = defaultEngine; + } + + if (engineObj) { + engine = engineObj.id; + descr = ''; + url = ''; + } else { + engine = ''; + descr = typeof settings.descr === 'string' ? settings.descr : ''; + url = typeof settings.url === 'string' ? settings.url : ''; + } } else { engine = ''; - descr = typeof settings.descr === 'string' ? settings.descr : ''; - url = typeof settings.url === 'string' ? settings.url : ''; + descr = typeof settings.descr === 'string' ? settings.descr : 'Search'; + url = ''; } + return { + mode: mode, engine, descr, url, @@ -82,6 +108,14 @@ export const createSettingsState: CreateSettingsState = (settings) => function SettingsEditorComp({settings, settingsApi}: SettingsEditorReactComponentProps) { const {updateSettings} = settingsApi; + function updMode(newModeId: string) { + const val = Number.parseInt(newModeId); + updateSettings({ + ...settings, + descr: (settings.mode === SettingsMode.Browser && enginesById[settings.engine]?.descr) || settings.descr, + mode: isSettingsMode(val) ? val : SettingsMode.Browser + }) + } function updEngine(newEngineId: string) { if (newEngineId === '') { const curEngineObj = enginesById[settings.engine]; @@ -109,6 +143,22 @@ function SettingsEditorComp({settings, settingsApi}: SettingsEditorReactComponen return ( <> + + + + {settings.mode === SettingsMode.Browser && - + } { - settings.engine==='' + settings.mode !== SettingsMode.Browser || settings.engine==='' ? - : } - + } strWithQuery.replaceAll(queryPlaceholder, queryVal); + function WidgetComp({settings, widgetApi}: WidgetReactComponentProps) { const [typedQuery, setTypedQuery] = useState(''); - const { shell } = widgetApi; + const { shell, widgets } = widgetApi; const {descr, urlTpl, queryTpl, notConfigNotes} = useMemo(() => { const engineId = settings.engine; + const modeId = settings.mode; let descr = '' let urlTpl = ''; const notConfigNotes: string[] = []; - if (engineId !== '') { - const engineObj = enginesById[engineId] - if (engineObj) { - descr = engineObj.descr; - urlTpl = engineObj.url; + if (modeId === SettingsMode.Browser) { + if (engineId !== '') { + const engineObj = enginesById[engineId] + if (engineObj) { + descr = engineObj.descr; + urlTpl = engineObj.url; + } else { + descr = defaultEngine.descr; + urlTpl = defaultEngine.url; + } } else { - descr = defaultEngine.descr; - urlTpl = defaultEngine.url; + descr = settings.descr; + urlTpl = sanitizeUrl(settings.url); + if(urlTpl==='') { + notConfigNotes.push('Invalid URL template') + } else if (urlTpl.indexOf(queryPlaceholder)<0) { + notConfigNotes.push('Missing QUERY in URL template') + } } } else { descr = settings.descr; - urlTpl = sanitizeUrl(settings.url); - if(urlTpl==='') { - notConfigNotes.push('Invalid URL template') - } else if (urlTpl.indexOf(queryPlaceholder)<0) { - notConfigNotes.push('Missing QUERY in URL template') - } } const queryTpl = settings.query.trim(); @@ -47,23 +55,41 @@ function WidgetComp({settings, widgetApi}: WidgetReactComponentProps) } return {descr, urlTpl, queryTpl, notConfigNotes} - }, [settings.descr, settings.engine, settings.query, settings.url]) + }, [settings.descr, settings.engine, settings.mode, settings.query, settings.url]) const onQuerySubmit = useMemo(() => { if (notConfigNotes.length>0) { return (_: FormEvent) => undefined; } else { - const finalQuery = queryTpl === '' ? typedQuery : queryTpl.replaceAll(queryPlaceholder, typedQuery); + const finalQuery = queryTpl === '' ? typedQuery : replaceQueryPlaceholder(queryTpl, typedQuery); const queryForUrl = encodeURIComponent(finalQuery); - const finalUrl = urlTpl.replaceAll(queryPlaceholder, queryForUrl); return (e: FormEvent) => { e.preventDefault(); setTypedQuery(''); - shell.openExternalUrl(finalUrl); + switch (settings.mode) { + case SettingsMode.Browser: { + const finalUrl = replaceQueryPlaceholder(urlTpl, queryForUrl); + shell.openExternalUrl(finalUrl); + break; + } + case SettingsMode.Webpages: { + const webpageWidgets = widgets.getWidgetsInCurrentWorkflow('webpage'); + for (const {api} of webpageWidgets) { + if (api.getUrl && api.openUrl) { + const tplUrl = api.getUrl(); + const finalUrl = replaceQueryPlaceholder(tplUrl, queryForUrl); + if (tplUrl!==finalUrl) { + api.openUrl(finalUrl); + } + } + } + break; + } + } } } - }, [notConfigNotes.length, queryTpl, shell, typedQuery, urlTpl]) + }, [notConfigNotes.length, queryTpl, settings.mode, shell, typedQuery, urlTpl, widgets]) return notConfigNotes.length===0 ?
diff --git a/src/renderer/widgets/webpage/widget.tsx b/src/renderer/widgets/webpage/widget.tsx index 9af7770..3a6173b 100644 --- a/src/renderer/widgets/webpage/widget.tsx +++ b/src/renderer/widgets/webpage/widget.tsx @@ -14,6 +14,7 @@ import { createContextMenuFactory } from '@/widgets/webpage/contextMenu'; import { ContextMenuEvent as ElectronContextMenuEvent } from 'electron'; import { createPartition } from '@/widgets/webpage/partition'; import { reload } from '@/widgets/webpage/actions'; +import { WebpageExposedApi } from '@/widgets/interfaces'; interface WebviewProps extends WidgetReactComponentProps { /** @@ -42,7 +43,7 @@ function Webview({settings, widgetApi, onRequireRestart, env, id}: WebviewProps) } }, [onRequireRestart, partition, reqRestartIfChanged]) - const {updateActionBar, setContextMenuFactory} = widgetApi; + const {updateActionBar, setContextMenuFactory, exposeApi} = widgetApi; const webviewRef = useRef(null); const [isLoading, setIsLoading] = useState(false); const [webviewIsReady, setWebviewIsReady] = useState(false); @@ -52,6 +53,13 @@ function Webview({settings, widgetApi, onRequireRestart, env, id}: WebviewProps) const sanitUrl = useMemo(() => sanitizeUrl(url), [url]); const sanitUA = useMemo(() => userAgent.trim(), [userAgent]); + useEffect(() => { + exposeApi({ + openUrl: (url: string) => webviewRef.current?.loadURL(url), + getUrl: () => url, + }) + }, [exposeApi, url]) + const refreshActions = useCallback( () => updateActionBar( createActionBarItems( diff --git a/tests/renderer/application/useCases/widget/getWidgetApi.spec.ts b/tests/renderer/application/useCases/widget/getWidgetApi.spec.ts index aa82841..dfbca29 100644 --- a/tests/renderer/application/useCases/widget/getWidgetApi.spec.ts +++ b/tests/renderer/application/useCases/widget/getWidgetApi.spec.ts @@ -49,12 +49,15 @@ function setup() { execCmdLines: jest.fn() } + const getWidgetsInCurrentWorkflowUseCase = jest.fn(); + const getWidgetApiUseCase = createGetWidgetApiUseCase({ clipboardProvider, processProvider, shellProvider, widgetDataStorageManager, terminalProvider, + getWidgetsInCurrentWorkflowUseCase, }); return { clipboardProvider, @@ -63,6 +66,7 @@ function setup() { widgetDataStorage, widgetDataStorageManager, terminalProvider, + getWidgetsInCurrentWorkflowUseCase, getWidgetApiUseCase } @@ -73,15 +77,18 @@ describe('getWidgetApiUseCase()', () => { [[], { updateActionBar: expect.any(Function), setContextMenuFactory: expect.any(Function), + exposeApi: expect.any(Function), }], [['clipboard'], { updateActionBar: expect.any(Function), setContextMenuFactory: expect.any(Function), + exposeApi: expect.any(Function), clipboard: expect.any(Object) }], [['dataStorage', 'shell'], { updateActionBar: expect.any(Function), setContextMenuFactory: expect.any(Function), + exposeApi: expect.any(Function), dataStorage: expect.any(Object), shell: expect.any(Object) }], @@ -90,7 +97,7 @@ describe('getWidgetApiUseCase()', () => { getWidgetApiUseCase } = setup() - const widgetApi = getWidgetApiUseCase('WIDGET-ID', false, () => undefined, () => undefined, requiredModules); + const widgetApi = getWidgetApiUseCase('WIDGET-ID', false, () => undefined, () => undefined, () => undefined, requiredModules); expect(widgetApi).toEqual(expectWidgetApi); }) @@ -102,16 +109,21 @@ describe('getWidgetApiUseCase()', () => { const testVal = 'TEST-VALUE'; const updateWidgetActionBarHandler = jest.fn(); const setContextMenuFactoryHandler = jest.fn(); + const exposeApiHandler = jest.fn(); - const widgetApi = getWidgetApiUseCase(widgetId, false, updateWidgetActionBarHandler, setContextMenuFactoryHandler, []); + const widgetApi = getWidgetApiUseCase(widgetId, false, updateWidgetActionBarHandler, setContextMenuFactoryHandler, exposeApiHandler, []); widgetApi.setContextMenuFactory(testVal as unknown as WidgetContextMenuFactory); - expect(setContextMenuFactoryHandler).toBeCalledTimes(1); - expect(setContextMenuFactoryHandler).toBeCalledWith(testVal); + expect(setContextMenuFactoryHandler).toHaveBeenCalledTimes(1); + expect(setContextMenuFactoryHandler).toHaveBeenCalledWith(testVal); widgetApi.updateActionBar(testVal as unknown as ActionBarItems); - expect(updateWidgetActionBarHandler).toBeCalledTimes(1); - expect(updateWidgetActionBarHandler).toBeCalledWith(testVal); + expect(updateWidgetActionBarHandler).toHaveBeenCalledTimes(1); + expect(updateWidgetActionBarHandler).toHaveBeenCalledWith(testVal); + + widgetApi.exposeApi(testVal as unknown as object); + expect(exposeApiHandler).toHaveBeenCalledTimes(1); + expect(exposeApiHandler).toHaveBeenCalledWith(testVal); }) it('should correctly setup common properties, when forPreview is true', () => { @@ -121,14 +133,18 @@ describe('getWidgetApiUseCase()', () => { const testVal = 'TEST-VALUE'; const updateWidgetActionBarHandler = jest.fn(); const setContextMenuFactoryHandler = jest.fn(); + const exposeApiHandler = jest.fn(); - const widgetApi = getWidgetApiUseCase(widgetId, true, updateWidgetActionBarHandler, setContextMenuFactoryHandler, []); + const widgetApi = getWidgetApiUseCase(widgetId, true, updateWidgetActionBarHandler, setContextMenuFactoryHandler, exposeApiHandler, []); widgetApi.setContextMenuFactory(testVal as unknown as WidgetContextMenuFactory); - expect(setContextMenuFactoryHandler).not.toBeCalled(); + expect(setContextMenuFactoryHandler).not.toHaveBeenCalled(); widgetApi.updateActionBar(testVal as unknown as ActionBarItems); - expect(updateWidgetActionBarHandler).not.toBeCalled(); + expect(updateWidgetActionBarHandler).not.toHaveBeenCalled(); + + widgetApi.exposeApi(testVal as unknown as object); + expect(exposeApiHandler).not.toHaveBeenCalled(); }) it('should correctly setup clipboard module', () => { @@ -137,15 +153,15 @@ describe('getWidgetApiUseCase()', () => { clipboardProvider } = setup() - const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, ['clipboard']); + const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, () => undefined, ['clipboard']); widgetApi.clipboard.writeBookmark('title', 'url'); - expect(clipboardProvider.writeBookmark).toBeCalledTimes(1); - expect(clipboardProvider.writeBookmark).toBeCalledWith('title', 'url'); + expect(clipboardProvider.writeBookmark).toHaveBeenCalledTimes(1); + expect(clipboardProvider.writeBookmark).toHaveBeenCalledWith('title', 'url'); widgetApi.clipboard.writeText('text'); - expect(clipboardProvider.writeText).toBeCalledTimes(1); - expect(clipboardProvider.writeText).toBeCalledWith('text'); + expect(clipboardProvider.writeText).toHaveBeenCalledTimes(1); + expect(clipboardProvider.writeText).toHaveBeenCalledWith('text'); }) it('should correctly setup dataStorage module', async () => { @@ -154,25 +170,25 @@ describe('getWidgetApiUseCase()', () => { widgetDataStorageManager } = setup() const testVal = 'TEST-VALUE'; - const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, ['dataStorage']); + const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, () => undefined, ['dataStorage']); const widgetDataStorage = await widgetDataStorageManager.getObject(widgetId); await widgetApi.dataStorage.clear(); - expect(widgetDataStorage.clear).toBeCalledTimes(1); - expect(widgetDataStorage.clear).toBeCalledWith(); + expect(widgetDataStorage.clear).toHaveBeenCalledTimes(1); + expect(widgetDataStorage.clear).toHaveBeenCalledWith(); widgetDataStorage.getText.mockResolvedValue(testVal); expect(await widgetApi.dataStorage.getText('key')).toBe(testVal); - expect(widgetDataStorage.getText).toBeCalledTimes(1); - expect(widgetDataStorage.getText).toBeCalledWith('key'); + expect(widgetDataStorage.getText).toHaveBeenCalledTimes(1); + expect(widgetDataStorage.getText).toHaveBeenCalledWith('key'); await widgetApi.dataStorage.remove('key'); - expect(widgetDataStorage.deleteItem).toBeCalledTimes(1); - expect(widgetDataStorage.deleteItem).toBeCalledWith('key'); + expect(widgetDataStorage.deleteItem).toHaveBeenCalledTimes(1); + expect(widgetDataStorage.deleteItem).toHaveBeenCalledWith('key'); await widgetApi.dataStorage.setText('key', 'value'); - expect(widgetDataStorage.setText).toBeCalledTimes(1); - expect(widgetDataStorage.setText).toBeCalledWith('key', 'value'); + expect(widgetDataStorage.setText).toHaveBeenCalledTimes(1); + expect(widgetDataStorage.setText).toHaveBeenCalledWith('key', 'value'); }) it('should correctly setup process module', () => { @@ -181,13 +197,13 @@ describe('getWidgetApiUseCase()', () => { processProvider } = setup() - const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, ['process']); + const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, () => undefined, ['process']); const processInfo = { some: 'info' } as unknown as ProcessInfo; processProvider.getProcessInfo.mockReturnValue(processInfo) expect(widgetApi.process.getProcessInfo()).toBe(processInfo); - expect(processProvider.getProcessInfo).toBeCalledTimes(1); - expect(processProvider.getProcessInfo).toBeCalledWith(); + expect(processProvider.getProcessInfo).toHaveBeenCalledTimes(1); + expect(processProvider.getProcessInfo).toHaveBeenCalledWith(); }) it('should correctly setup shell module', () => { @@ -196,19 +212,19 @@ describe('getWidgetApiUseCase()', () => { shellProvider } = setup() - const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, ['shell']); + const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, () => undefined, ['shell']); widgetApi.shell.openApp('app/path', ['arg1', 'arg2']); - expect(shellProvider.openApp).toBeCalledTimes(1); - expect(shellProvider.openApp).toBeCalledWith('app/path', ['arg1', 'arg2']); + expect(shellProvider.openApp).toHaveBeenCalledTimes(1); + expect(shellProvider.openApp).toHaveBeenCalledWith('app/path', ['arg1', 'arg2']); widgetApi.shell.openExternalUrl('test://url'); - expect(shellProvider.openExternal).toBeCalledTimes(1); - expect(shellProvider.openExternal).toBeCalledWith('test://url'); + expect(shellProvider.openExternal).toHaveBeenCalledTimes(1); + expect(shellProvider.openExternal).toHaveBeenCalledWith('test://url'); widgetApi.shell.openPath('some/file/path'); - expect(shellProvider.openPath).toBeCalledTimes(1); - expect(shellProvider.openPath).toBeCalledWith('some/file/path'); + expect(shellProvider.openPath).toHaveBeenCalledTimes(1); + expect(shellProvider.openPath).toHaveBeenCalledWith('some/file/path'); }) it('should correctly setup terminal module', () => { @@ -217,11 +233,24 @@ describe('getWidgetApiUseCase()', () => { terminalProvider } = setup() - const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, ['terminal']); + const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, () => undefined, ['terminal']); widgetApi.terminal.execCmdLines(['cmd1', 'cmd2'], 'cwd'); - expect(terminalProvider.execCmdLines).toBeCalledTimes(1); - expect(terminalProvider.execCmdLines).toBeCalledWith(['cmd1', 'cmd2'], 'cwd'); + expect(terminalProvider.execCmdLines).toHaveBeenCalledTimes(1); + expect(terminalProvider.execCmdLines).toHaveBeenCalledWith(['cmd1', 'cmd2'], 'cwd'); + }) + + it('should correctly setup widgets module', () => { + const { + getWidgetApiUseCase, + getWidgetsInCurrentWorkflowUseCase + } = setup() + + const widgetApi = getWidgetApiUseCase(widgetId, false, () => undefined, () => undefined, () => undefined, ['widgets']); + + widgetApi.widgets.getWidgetsInCurrentWorkflow('widget-type'); + expect(getWidgetsInCurrentWorkflowUseCase).toHaveBeenCalledTimes(1); + expect(getWidgetsInCurrentWorkflowUseCase).toHaveBeenCalledWith('widget-type'); }) }) diff --git a/tests/renderer/application/useCases/widget/setExposedApi.spec.ts b/tests/renderer/application/useCases/widget/setExposedApi.spec.ts new file mode 100644 index 0000000..cd5f0c6 --- /dev/null +++ b/tests/renderer/application/useCases/widget/setExposedApi.spec.ts @@ -0,0 +1,75 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { createSetExposedApiUseCase } from '@/application/useCases/widget/setExposedApi'; +import { AppState } from '@/base/state/app'; +import { fixtureAppState } from '@tests/base/state/fixtures/appState'; +import { fixtureAppStore } from '@tests/data/fixtures/appStore'; +import { fixtureWidgetA } from '@tests/base/fixtures/widget'; + +async function setup(initState: AppState) { + const [appStore] = await fixtureAppStore(initState); + const setExposedApiUseCase = createSetExposedApiUseCase({ + appStore, + }); + return { + appStore, + setExposedApiUseCase, + } +} + +describe('setExposedApiUseCase()', () => { + it('should do nothing, if no such widget exists', async () => { + const widgetA = fixtureWidgetA({ exposedApi: {} }) + const initState = fixtureAppState({ + entities: { + widgets: { + [widgetA.id]: widgetA + }, + } + }) + const { + appStore, + setExposedApiUseCase + } = await setup(initState) + const expectState = appStore.get(); + + setExposedApiUseCase('NO-SUCH-ID', { 'some': 'obj' }); + + expect(appStore.get()).toBe(expectState); + }) + + it('should update the exposedApi object for the widget', async () => { + const widgetA = fixtureWidgetA({ exposedApi: {} }) + const newExposedApi = { 'some': 'object' }; + const initState = fixtureAppState({ + entities: { + widgets: { + [widgetA.id]: widgetA + }, + } + }) + const expectState: AppState = { + ...initState, + entities: { + ...initState.entities, + widgets: { + [widgetA.id]: { + ...widgetA, + exposedApi: newExposedApi + } + } + } + } + const { + appStore, + setExposedApiUseCase + } = await setup(initState) + + setExposedApiUseCase(widgetA.id, newExposedApi); + + expect(appStore.get()).toEqual(expectState); + }) +}) diff --git a/tests/renderer/application/useCases/widget/widgetApiWidgets/getWidgetsInCurrentWorkflow.spec.ts b/tests/renderer/application/useCases/widget/widgetApiWidgets/getWidgetsInCurrentWorkflow.spec.ts new file mode 100644 index 0000000..9e5aa90 --- /dev/null +++ b/tests/renderer/application/useCases/widget/widgetApiWidgets/getWidgetsInCurrentWorkflow.spec.ts @@ -0,0 +1,272 @@ +/* + * Copyright: (c) 2024, Alex Kaul + * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + */ + +import { AppState } from '@/base/state/app'; +import { fixtureAppState } from '@tests/base/state/fixtures/appState'; +import { fixtureAppStore } from '@tests/data/fixtures/appStore'; +import { fixtureWidgetA, fixtureWidgetB, fixtureWidgetC, fixtureWidgetD } from '@tests/base/fixtures/widget'; +import { createGetWidgetsInCurrentWorkflowUseCase } from '@/application/useCases/widget/widgetApiWidgets/getWidgetsInCurrentWorkflow'; +import { fixtureWorkflowA, fixtureWorkflowB } from '@tests/base/fixtures/workflow'; +import { fixtureProjectA } from '@tests/base/fixtures/project'; +import { fixtureWidgetLayoutItemA, fixtureWidgetLayoutItemB, fixtureWidgetLayoutItemC, fixtureWidgetLayoutItemD } from '@tests/base/fixtures/widgetLayout'; +import { fixtureWidgetTypeA, fixtureWidgetTypeB } from '@tests/base/fixtures/widgetType'; +import { fixtureProjectSwitcher } from '@tests/base/state/fixtures/projectSwitcher'; + +async function setup(initState: AppState) { + const [appStore] = await fixtureAppStore(initState); + const getWidgetsInCurrentWorkflowUseCase = createGetWidgetsInCurrentWorkflowUseCase({ + appStore, + }); + return { + appStore, + getWidgetsInCurrentWorkflowUseCase, + } +} + +describe('getWidgetsInCurrentWorkflowUseCase()', () => { + it('should return empty array, if there is no current project', async () => { + const widgetTypeA = fixtureWidgetTypeA(); + const widgetA = fixtureWidgetA({ type: widgetTypeA.id }); + const workflowA = fixtureWorkflowA({ layout: [fixtureWidgetLayoutItemA({ widgetId: widgetA.id })] }); + const projectA = fixtureProjectA({ currentWorkflowId: workflowA.id, workflowIds: [workflowA.id] }); + const initState = fixtureAppState({ + entities: { + projects: { + [projectA.id]: projectA + }, + widgets: { + [widgetA.id]: widgetA + }, + widgetTypes: { + [widgetTypeA.id]: widgetTypeA + }, + workflows: { + [workflowA.id]: workflowA + } + }, + ui: { + projectSwitcher: fixtureProjectSwitcher({ + currentProjectId: 'NO-SUCH-ID' + }) + } + }) + const { + getWidgetsInCurrentWorkflowUseCase + } = await setup(initState) + + const res = getWidgetsInCurrentWorkflowUseCase(widgetTypeA.id); + + expect(res).toEqual([]); + }) + + it('should return empty array, if there is no current workflow', async () => { + const widgetTypeA = fixtureWidgetTypeA(); + const widgetA = fixtureWidgetA({ type: widgetTypeA.id }); + const workflowA = fixtureWorkflowA({ layout: [fixtureWidgetLayoutItemA({ widgetId: widgetA.id })] }); + const projectA = fixtureProjectA({ currentWorkflowId: 'NO-SUCH-ID', workflowIds: [workflowA.id] }); + const initState = fixtureAppState({ + entities: { + projects: { + [projectA.id]: projectA + }, + widgets: { + [widgetA.id]: widgetA + }, + widgetTypes: { + [widgetTypeA.id]: widgetTypeA + }, + workflows: { + [workflowA.id]: workflowA + } + }, + ui: { + projectSwitcher: fixtureProjectSwitcher({ + currentProjectId: projectA.id + }) + } + }) + const { + getWidgetsInCurrentWorkflowUseCase + } = await setup(initState) + + const res = getWidgetsInCurrentWorkflowUseCase(widgetTypeA.id); + + expect(res).toEqual([]); + }) + + it('should return empty array, if there is no specified widget type', async () => { + const widgetTypeA = fixtureWidgetTypeA(); + const widgetA = fixtureWidgetA({ type: widgetTypeA.id }); + const workflowA = fixtureWorkflowA({ layout: [fixtureWidgetLayoutItemA({ widgetId: widgetA.id })] }); + const projectA = fixtureProjectA({ currentWorkflowId: workflowA.id, workflowIds: [workflowA.id] }); + const initState = fixtureAppState({ + entities: { + projects: { + [projectA.id]: projectA + }, + widgets: { + [widgetA.id]: widgetA + }, + widgetTypes: { + [widgetTypeA.id]: widgetTypeA + }, + workflows: { + [workflowA.id]: workflowA + } + }, + ui: { + projectSwitcher: fixtureProjectSwitcher({ + currentProjectId: projectA.id + }) + } + }) + const { + getWidgetsInCurrentWorkflowUseCase + } = await setup(initState) + + const res = getWidgetsInCurrentWorkflowUseCase('NO-SUCH-ID'); + + expect(res).toEqual([]); + }) + + it('should return data for widgets of the specified type on the current workflow only', async () => { + const widgetTypeA = fixtureWidgetTypeA(); + const widgetTypeB = fixtureWidgetTypeB(); + const widgetA = fixtureWidgetA({ type: widgetTypeA.id }); + const widgetB = fixtureWidgetB({ type: widgetTypeB.id }); + const widgetC = fixtureWidgetC({ type: widgetTypeA.id }); + const widgetD = fixtureWidgetD({ type: widgetTypeA.id }); + const workflowA = fixtureWorkflowA({ + layout: [ + fixtureWidgetLayoutItemA({ widgetId: widgetA.id }), + fixtureWidgetLayoutItemB({ widgetId: widgetB.id }), + fixtureWidgetLayoutItemC({ widgetId: widgetC.id }) + ] + }); + const workflowB = fixtureWorkflowB({ layout: [fixtureWidgetLayoutItemD({ widgetId: widgetD.id })] }) + const projectA = fixtureProjectA({ currentWorkflowId: workflowA.id, workflowIds: [workflowA.id, workflowB.id] }); + const initState = fixtureAppState({ + entities: { + projects: { + [projectA.id]: projectA + }, + widgets: { + [widgetA.id]: widgetA, + [widgetB.id]: widgetB, + [widgetC.id]: widgetC + }, + widgetTypes: { + [widgetTypeA.id]: widgetTypeA, + [widgetTypeB.id]: widgetTypeB, + }, + workflows: { + [workflowA.id]: workflowA, + [workflowB.id]: workflowB, + } + }, + ui: { + projectSwitcher: fixtureProjectSwitcher({ + currentProjectId: projectA.id + }) + } + }) + const { + getWidgetsInCurrentWorkflowUseCase + } = await setup(initState) + + const res = getWidgetsInCurrentWorkflowUseCase(widgetTypeA.id); + + expect(res).toEqual([ + expect.objectContaining({ id: widgetA.id }), + expect.objectContaining({ id: widgetC.id }), + ]); + }) + + it('should return id, name and exposed api of widgets', async () => { + const widgetTypeA = fixtureWidgetTypeA(); + const widgetA = fixtureWidgetA({ type: widgetTypeA.id, exposedApi: { some: 'object' } }); + const workflowA = fixtureWorkflowA({ + layout: [ + fixtureWidgetLayoutItemA({ widgetId: widgetA.id }), + ] + }); + const projectA = fixtureProjectA({ currentWorkflowId: workflowA.id, workflowIds: [workflowA.id] }); + const initState = fixtureAppState({ + entities: { + projects: { + [projectA.id]: projectA + }, + widgets: { + [widgetA.id]: widgetA, + }, + widgetTypes: { + [widgetTypeA.id]: widgetTypeA, + }, + workflows: { + [workflowA.id]: workflowA, + } + }, + ui: { + projectSwitcher: fixtureProjectSwitcher({ + currentProjectId: projectA.id + }) + } + }) + const { + getWidgetsInCurrentWorkflowUseCase + } = await setup(initState) + + const res = getWidgetsInCurrentWorkflowUseCase(widgetTypeA.id); + + expect(res).toEqual([{ + id: widgetA.id, + name: widgetA.coreSettings.name, + api: widgetA.exposedApi + }]); + }) + + it('should return empty api object, if the widget does not expose any api', async () => { + const widgetTypeA = fixtureWidgetTypeA(); + const { exposedApi: _, ...widgetA } = fixtureWidgetA({ type: widgetTypeA.id }); + const workflowA = fixtureWorkflowA({ + layout: [ + fixtureWidgetLayoutItemA({ widgetId: widgetA.id }), + ] + }); + const projectA = fixtureProjectA({ currentWorkflowId: workflowA.id, workflowIds: [workflowA.id] }); + const initState = fixtureAppState({ + entities: { + projects: { + [projectA.id]: projectA + }, + widgets: { + [widgetA.id]: widgetA, + }, + widgetTypes: { + [widgetTypeA.id]: widgetTypeA, + }, + workflows: { + [workflowA.id]: workflowA, + } + }, + ui: { + projectSwitcher: fixtureProjectSwitcher({ + currentProjectId: projectA.id + }) + } + }) + const { + getWidgetsInCurrentWorkflowUseCase + } = await setup(initState) + + const res = getWidgetsInCurrentWorkflowUseCase(widgetTypeA.id); + + expect(res).toEqual([{ + id: widgetA.id, + name: widgetA.coreSettings.name, + api: {} + }]); + }) +}) diff --git a/tests/renderer/base/state/app.spec.ts b/tests/renderer/base/state/app.spec.ts index a954df8..e3dbf32 100644 --- a/tests/renderer/base/state/app.spec.ts +++ b/tests/renderer/base/state/app.spec.ts @@ -97,7 +97,7 @@ describe('AppState', () => { describe('createPersistentAppState', () => { it('creates PersistentAppState by picking props from AppState that should be persisted', () => { - const widgetA = fixtureWidgetA({}); + const widgetA = fixtureWidgetA({ exposedApi: { some: 'object' } }); const state = fixtureAppState({ entities: { apps: fixtureAppAInColl(), @@ -109,7 +109,7 @@ describe('AppState', () => { workflows: fixtureWorkflowAInColl() }, }); - const { ...persistentWidgetA } = widgetA; + const { exposedApi: _, ...persistentWidgetA } = widgetA; const expectPersistentState: PersistentAppState = { entities: { apps: state.entities.apps, diff --git a/tests/renderer/base/widgetApi.spec.ts b/tests/renderer/base/widgetApi.spec.ts index bb87b3c..0e60881 100644 --- a/tests/renderer/base/widgetApi.spec.ts +++ b/tests/renderer/base/widgetApi.spec.ts @@ -26,12 +26,13 @@ describe('WidgetApi', () => { const commonFactory = jest.fn(); const updateActionBarHandler = () => undefined; const setContextMenuFactoryHandler = () => undefined; + const exposeApiHandler = () => undefined; const widgetApiFactory = callCreateWidgetApiFactory({ commonFactory }); - widgetApiFactory(testId, updateActionBarHandler, setContextMenuFactoryHandler, []); + widgetApiFactory(testId, updateActionBarHandler, setContextMenuFactoryHandler, exposeApiHandler, []); - expect(commonFactory).toBeCalledTimes(1); - expect(commonFactory).toBeCalledWith(testId, updateActionBarHandler, setContextMenuFactoryHandler); + expect(commonFactory).toHaveBeenCalledTimes(1); + expect(commonFactory).toHaveBeenCalledWith(testId, updateActionBarHandler, setContextMenuFactoryHandler, exposeApiHandler); }) it('should put props returned by commonFactory into WidgetApi', () => { const testProps = { @@ -40,7 +41,7 @@ describe('WidgetApi', () => { }; const widgetApiFactory = callCreateWidgetApiFactory({ commonFactory: () => testProps }); - const gotRes = widgetApiFactory('', () => undefined, () => undefined, []); + const gotRes = widgetApiFactory('', () => undefined, () => undefined, () => undefined, []); expect(gotRes).toMatchObject(testProps); }) @@ -49,10 +50,10 @@ describe('WidgetApi', () => { const moduleName: WidgetApiModuleName = 'clipboard'; const widgetApiFactory = callCreateWidgetApiFactory({ moduleFactories: { [moduleName]: moduleFactory } }); - widgetApiFactory(testId, () => undefined, () => undefined, [moduleName]); + widgetApiFactory(testId, () => undefined, () => undefined, () => undefined, [moduleName]); - expect(moduleFactory).toBeCalledTimes(1); - expect(moduleFactory).toBeCalledWith(testId); + expect(moduleFactory).toHaveBeenCalledTimes(1); + expect(moduleFactory).toHaveBeenCalledWith(testId); }) it('should put module objects returned by moduleFactories into WidgetApi', () => { const testProps = { @@ -62,7 +63,7 @@ describe('WidgetApi', () => { const moduleName: WidgetApiModuleName = 'clipboard'; const widgetApiFactory = callCreateWidgetApiFactory({ moduleFactories: { [moduleName]: () => testProps } }); - const gotRes = widgetApiFactory('', () => undefined, () => undefined, [moduleName]); + const gotRes = widgetApiFactory('', () => undefined, () => undefined, () => undefined, [moduleName]); expect(gotRes).toMatchObject({ [moduleName]: testProps @@ -92,7 +93,7 @@ describe('WidgetApi', () => { } }); - const gotRes = widgetApiFactory('', () => undefined, () => undefined, [moduleName2, moduleName3]); + const gotRes = widgetApiFactory('', () => undefined, () => undefined, () => undefined, [moduleName2, moduleName3]); expect(gotRes).toMatchObject({ [moduleName2]: testProps2, diff --git a/tests/renderer/ui/components/widget/widget.spec.tsx b/tests/renderer/ui/components/widget/widget.spec.tsx index a655d34..8c2915a 100644 --- a/tests/renderer/ui/components/widget/widget.spec.tsx +++ b/tests/renderer/ui/components/widget/widget.spec.tsx @@ -47,16 +47,19 @@ async function setup({ const showWidgetContextMenuUseCase = jest.fn(); const copyWidgetUseCase = jest.fn(); const showContextMenuUseCase = jest.fn(); + const setExposedApiUseCase = jest.fn(); const getWidgetApiUseCase = mocks?.getWidgetApiUseCase || ( ( _widgetId: string, _previewMode: boolean, updateActionBarHandler: (actionBarItems: ActionBarItems)=>void, - setContextMenuFactoryHandler: (contextMenuFactory: WidgetContextMenuFactory)=>void + setContextMenuFactoryHandler: (contextMenuFactory: WidgetContextMenuFactory)=>void, + exposeApiHandler: (api: object)=>void ) => { const widgetApi: Partial = { updateActionBar: (actionBarItems) => act(() => updateActionBarHandler(actionBarItems)), - setContextMenuFactory: (factory) => act(() => setContextMenuFactoryHandler(factory)) + setContextMenuFactory: (factory) => act(() => setContextMenuFactoryHandler(factory)), + exposeApi: (api) => act(()=>exposeApiHandler(api)) } return widgetApi as WidgetApi; } @@ -70,6 +73,7 @@ async function setup({ getWidgetApiUseCase, copyWidgetUseCase, showContextMenuUseCase, + setExposedApiUseCase, }) const Widget = createWidgetComponent({ useWidgetViewModel @@ -639,8 +643,8 @@ describe('', () => { const elButton = screen.getByRole('button', {name: /widget settings/i}); fireEvent.click(elButton); - expect(openWidgetSettingsUseCase).toBeCalledTimes(1); - expect(openWidgetSettingsUseCase).toBeCalledWith(widgetId, expect.objectContaining({ + expect(openWidgetSettingsUseCase).toHaveBeenCalledTimes(1); + expect(openWidgetSettingsUseCase).toHaveBeenCalledWith(widgetId, expect.objectContaining({ area: 'shelf' })) }) @@ -670,8 +674,8 @@ describe('', () => { const elButton = screen.getByRole('button', {name: /delete widget/i}); fireEvent.click(elButton); - expect(deleteWidgetUseCase).toBeCalledTimes(1); - expect(deleteWidgetUseCase).toBeCalledWith(widgetId, env); + expect(deleteWidgetUseCase).toHaveBeenCalledTimes(1); + expect(deleteWidgetUseCase).toHaveBeenCalledWith(widgetId, env); }) it('should display a warning and nothing else, if the widget has an unexisting type', async () => { @@ -848,8 +852,8 @@ describe('', () => { } }); - expect(getWidgetApiUseCase).toBeCalledTimes(1); - expect(getWidgetApiUseCase).toBeCalledWith(widgetId, false, expect.any(Function), expect.any(Function), testRequiresApi); + expect(getWidgetApiUseCase).toHaveBeenCalledTimes(1); + expect(getWidgetApiUseCase).toHaveBeenCalledWith(widgetId, false, expect.any(Function), expect.any(Function), expect.any(Function), testRequiresApi); expect(screen.queryByText(getWidgetApiUseCaseRes)).toBeInTheDocument(); }) @@ -876,8 +880,8 @@ describe('', () => { fireEvent.contextMenu(screen.getByTestId(testId1)); - expect(showWidgetContextMenuUseCase).toBeCalledTimes(1); - expect(showWidgetContextMenuUseCase).toBeCalledWith(widgetId, undefined, '', undefined); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledTimes(1); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledWith(widgetId, undefined, '', undefined); }) it('should call showWidgetContextMenuUseCase with a contextMenuFactory set by the widget, when a context menu is called', async () => { @@ -909,8 +913,8 @@ describe('', () => { fireEvent.contextMenu(screen.getByTestId(testId1)); - expect(showWidgetContextMenuUseCase).toBeCalledTimes(1); - expect(showWidgetContextMenuUseCase).toBeCalledWith(widgetId, contextMenuFactory, '', undefined); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledTimes(1); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledWith(widgetId, contextMenuFactory, '', undefined); }) it('should call showWidgetContextMenuUseCase with contextId==="", when a context menu is called and no Comp elements have a data-widget-context attribute', async () => { @@ -936,8 +940,8 @@ describe('', () => { fireEvent.contextMenu(screen.getByTestId(testId1)); - expect(showWidgetContextMenuUseCase).toBeCalledTimes(1); - expect(showWidgetContextMenuUseCase).toBeCalledWith(widgetId, undefined, '', undefined); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledTimes(1); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledWith(widgetId, undefined, '', undefined); }) it('should call showWidgetContextMenuUseCase with contextId specified by "data-widget-context" attribute of an element where a context menu is called', async () => { @@ -964,8 +968,8 @@ describe('', () => { fireEvent.contextMenu(screen.getByTestId(testId1)); - expect(showWidgetContextMenuUseCase).toBeCalledTimes(1); - expect(showWidgetContextMenuUseCase).toBeCalledWith(widgetId, undefined, contextId, undefined); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledTimes(1); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledWith(widgetId, undefined, contextId, undefined); }) it('should call showWidgetContextMenuUseCase with contextId specified by "data-widget-context" attribute of a parent element, if an element where a context menu is called does not have it', async () => { @@ -992,8 +996,8 @@ describe('', () => { fireEvent.contextMenu(screen.getByTestId(testId1)); - expect(showWidgetContextMenuUseCase).toBeCalledTimes(1); - expect(showWidgetContextMenuUseCase).toBeCalledWith(widgetId, undefined, contextId, undefined); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledTimes(1); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledWith(widgetId, undefined, contextId, undefined); }) it('should call showWidgetContextMenuUseCase with contextData specified in Event object, when the widget fires a custom contextmenu event with `contextData` property', async () => { @@ -1022,8 +1026,8 @@ describe('', () => { evt.contextData = testContextData; fireEvent(screen.getByTestId(testId1), evt); - expect(showWidgetContextMenuUseCase).toBeCalledTimes(1); - expect(showWidgetContextMenuUseCase).toBeCalledWith(widgetId, undefined, '', testContextData); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledTimes(1); + expect(showWidgetContextMenuUseCase).toHaveBeenCalledWith(widgetId, undefined, '', testContextData); }) }) diff --git a/tests/renderer/widgets/setupSut.tsx b/tests/renderer/widgets/setupSut.tsx index e43752a..6fecef1 100644 --- a/tests/renderer/widgets/setupSut.tsx +++ b/tests/renderer/widgets/setupSut.tsx @@ -90,6 +90,7 @@ export function setupWidgetSut(reactComp: ReactComponent(reactComp: ReactComponent): Settings { return { descr: 'Descr', engine: 'ddgo', + mode: SettingsMode.Browser, query: '', url: '', ...settings diff --git a/tests/renderer/widgets/web-query/settings.spec.ts b/tests/renderer/widgets/web-query/settings.spec.ts index ae9376c..04e4fd7 100644 --- a/tests/renderer/widgets/web-query/settings.spec.ts +++ b/tests/renderer/widgets/web-query/settings.spec.ts @@ -3,171 +3,301 @@ * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) */ -import { createSettingsState, defaultEngine, settingsEditorComp } from '@/widgets/web-query/settings'; +import { createSettingsState, defaultEngine, settingsEditorComp, SettingsMode } from '@/widgets/web-query/settings'; import { screen, waitFor } from '@testing-library/react'; import { setupSettingsSut } from '@tests/widgets/setupSut' import { fixtureSettings } from './fixtures'; describe('createSettingsState()', () => { - it('should correctly create settings state, when engine is not string', () => { - const { engine, ...srcSettings } = fixtureSettings({ descr: 'Descr', query: 'Query', url: 'Url' }) - const state = createSettingsState({ ...srcSettings }) - - expect(state.descr).toBe(''); - expect(state.engine).toBe(defaultEngine.id); - expect(state.query).toBe(srcSettings.query); - expect(state.url).toBe(''); - }) - - it('should correctly create settings state, when engine is custom (empty string)', () => { - const srcSettings = fixtureSettings({ engine: '', descr: 'Descr', query: 'Query', url: 'Url' }) + it('should correctly create settings state, when mode is invalid', () => { + const { mode, ...srcSettings } = fixtureSettings({ engine: '', descr: 'Descr', query: 'Query', url: 'Url' }) const state = createSettingsState({ ...srcSettings }) + expect(state.mode).toBe(SettingsMode.Browser); expect(state.descr).toBe(srcSettings.descr); - expect(state.engine).toBe(srcSettings.engine); + expect(state.engine).toBe(''); expect(state.query).toBe(srcSettings.query); expect(state.url).toBe(srcSettings.url); }) - it('should correctly create settings state, when engine is not custom', () => { - const srcSettings = fixtureSettings({ engine: 'ddgo-lite', descr: 'Descr', query: 'Query', url: 'Url' }) - const state = createSettingsState({ ...srcSettings }) - - expect(state.descr).toBe(''); - expect(state.engine).toBe(srcSettings.engine); - expect(state.query).toBe(srcSettings.query); - expect(state.url).toBe(''); + describe('Browser mode', () => { + it('should correctly create settings state, when engine is not string', () => { + const { engine, ...srcSettings } = fixtureSettings({ mode: SettingsMode.Browser, descr: 'Descr', query: 'Query', url: 'Url' }) + const state = createSettingsState({ ...srcSettings }) + + expect(state.mode).toBe(srcSettings.mode); + expect(state.descr).toBe(''); + expect(state.engine).toBe(defaultEngine.id); + expect(state.query).toBe(srcSettings.query); + expect(state.url).toBe(''); + }) + + it('should correctly create settings state, when engine is custom (empty string)', () => { + const srcSettings = fixtureSettings({ mode: SettingsMode.Browser, engine: '', descr: 'Descr', query: 'Query', url: 'Url' }) + const state = createSettingsState({ ...srcSettings }) + + expect(state.mode).toBe(srcSettings.mode); + expect(state.descr).toBe(srcSettings.descr); + expect(state.engine).toBe(srcSettings.engine); + expect(state.query).toBe(srcSettings.query); + expect(state.url).toBe(srcSettings.url); + }) + + it('should correctly create settings state, when engine is not custom', () => { + const srcSettings = fixtureSettings({ mode: SettingsMode.Browser, engine: 'ddgo-lite', descr: 'Descr', query: 'Query', url: 'Url' }) + const state = createSettingsState({ ...srcSettings }) + + expect(state.mode).toBe(srcSettings.mode); + expect(state.descr).toBe(''); + expect(state.engine).toBe(srcSettings.engine); + expect(state.query).toBe(srcSettings.query); + expect(state.url).toBe(''); + }) + + it('should correctly create settings state, when engine does not exist', () => { + const srcSettings = fixtureSettings({ mode: SettingsMode.Browser, engine: 'NO-SUCH-ID', descr: 'Descr', query: 'Query', url: 'Url' }) + const state = createSettingsState({ ...srcSettings }) + + expect(state.mode).toBe(srcSettings.mode); + expect(state.descr).toBe(''); + expect(state.engine).toBe(defaultEngine.id); + expect(state.query).toBe(srcSettings.query); + expect(state.url).toBe(''); + }) }) - it('should correctly create settings state, when engine does not exist', () => { - const srcSettings = fixtureSettings({ engine: 'NO-SUCH-ID', descr: 'Descr', query: 'Query', url: 'Url' }) - const state = createSettingsState({ ...srcSettings }) - - expect(state.descr).toBe(''); - expect(state.engine).toBe(defaultEngine.id); - expect(state.query).toBe(srcSettings.query); - expect(state.url).toBe(''); + describe('Webpages mode', () => { + it('should correctly create settings state', () => { + const srcSettings = fixtureSettings({ mode: SettingsMode.Webpages, descr: 'Descr', engine: '', query: 'Query', url: 'Url' }) + const state = createSettingsState({ ...srcSettings }) + + expect(state.mode).toBe(srcSettings.mode); + expect(state.descr).toBe(srcSettings.descr); + expect(state.engine).toBe(''); + expect(state.query).toBe(srcSettings.query); + expect(state.url).toBe(''); + }) }) }) describe('Web Query Widget Settings', () => { - it('should fill inputs with right values, when engine==custom', () => { - const settings = fixtureSettings({ engine: '', descr: 'Some Descr', query: 'Some Query', url: 'Some Url' }); - setupSettingsSut(settingsEditorComp, settings); - - expect(screen.getByRole('combobox', { name: /query engine/i })).toHaveValue(settings.engine); - expect(screen.getByRole('textbox', { name: /description/i })).toHaveValue(settings.descr); - expect(screen.getByRole('textbox', { name: /url template/i })).toHaveValue(settings.url); - expect(screen.getByRole('textbox', { name: /query template/i })).toHaveValue(settings.query); - }) - - it('should fill inputs with right values, when engine!==custom', () => { - const settings = fixtureSettings({ engine: defaultEngine.id, descr: 'Some Descr', query: 'Some Query', url: 'Some Url' }); + it('should fill Mode input with right value', () => { + const settings = fixtureSettings({ mode: SettingsMode.Webpages }); setupSettingsSut(settingsEditorComp, settings); - expect(screen.getByRole('combobox', { name: /query engine/i })).toHaveValue(settings.engine); - expect(screen.getByRole('textbox', { name: /description/i })).toHaveValue(defaultEngine.descr); - expect(screen.getByRole('textbox', { name: /url template/i })).toHaveValue(defaultEngine.url); - expect(screen.getByRole('textbox', { name: /query template/i })).toHaveValue(settings.query); + expect(screen.getByRole('combobox', { name: /mode/i })).toHaveValue(settings.mode.toString()); }) - it('should enable descr/url inputs, when engine==custom', () => { - const settings = fixtureSettings({ engine: '' }); - setupSettingsSut(settingsEditorComp, settings); - - expect(screen.getByRole('textbox', { name: /description/i })).toBeEnabled(); - expect(screen.getByRole('textbox', { name: /url template/i })).toBeEnabled(); - }) - - it('should disable descr/url inputs, when engine!==custom', () => { - const settings = fixtureSettings({ engine: defaultEngine.id }); - setupSettingsSut(settingsEditorComp, settings); - - expect(screen.getByRole('textbox', { name: /description/i })).toBeDisabled(); - expect(screen.getByRole('textbox', { name: /url template/i })).toBeDisabled(); - }) - - it('should allow to update "engine" setting with an option select', async () => { - const settings = fixtureSettings({ engine: defaultEngine.id }); + it('should allow to update "mode" setting with an option select', async () => { + const settings = fixtureSettings({ mode: SettingsMode.Webpages }); const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); - const select = screen.getByRole('combobox', { name: /query engine/i }) + const select = screen.getByRole('combobox', { name: /mode/i }) - userEvent.selectOptions(select, 'goog'); - await waitFor(() => expect((screen.getByRole('option', { name: 'Google' }) as HTMLOptionElement).selected).toBe(true)) + userEvent.selectOptions(select, SettingsMode.Browser.toString()); + await waitFor(() => expect((screen.getByRole('option', { name: 'Browser App' }) as HTMLOptionElement).selected).toBe(true)) expect(getSettings()).toEqual({ ...settings, - engine: 'goog' + mode: SettingsMode.Browser }); }) - it('should update descr/url with current engine\'s values, when switching "engine" setting from non-custom to custom', async () => { - const settings = fixtureSettings({ engine: defaultEngine.id }); + it('should update descr with current engine\'s value, when switching "mode" from Browser to Webpages', async () => { + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: defaultEngine.id }); const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); - const select = screen.getByRole('combobox', { name: /query engine/i }) + const select = screen.getByRole('combobox', { name: /mode/i }) - userEvent.selectOptions(select, ''); - await waitFor(() => expect((screen.getByRole('option', { name: 'Custom Engine' }) as HTMLOptionElement).selected).toBe(true)) + userEvent.selectOptions(select, SettingsMode.Webpages.toString()); + await waitFor(() => expect((screen.getByRole('option', { name: 'Webpage Widgets' }) as HTMLOptionElement).selected).toBe(true)) expect(getSettings()).toEqual({ ...settings, - engine: '', - descr: defaultEngine.descr, - url: defaultEngine.url + mode: SettingsMode.Webpages, + descr: defaultEngine.descr }); }) - it('should not update descr/url, when switching "engine" setting from custom to non-custom', async () => { - const settings = fixtureSettings({ engine: '' }); - const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); - const select = screen.getByRole('combobox', { name: /query engine/i }) - - userEvent.selectOptions(select, defaultEngine.id); - await waitFor(() => expect((screen.getByRole('option', { name: defaultEngine.name }) as HTMLOptionElement).selected).toBe(true)) - expect(getSettings()).toEqual({ - ...settings, - engine: defaultEngine.id, - }); + describe('Browser mode', () => { + it('should show right inputs', () => { + const settings = fixtureSettings({ mode: SettingsMode.Browser }); + setupSettingsSut(settingsEditorComp, settings); + + expect(screen.getByRole('combobox', { name: /query engine/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /description/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /url template/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /query template/i })).toBeInTheDocument(); + }) + + it('should fill inputs with right values, when engine==custom', () => { + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: '', descr: 'Some Descr', query: 'Some Query', url: 'Some Url' }); + setupSettingsSut(settingsEditorComp, settings); + + expect(screen.getByRole('combobox', { name: /query engine/i })).toHaveValue(settings.engine); + expect(screen.getByRole('textbox', { name: /description/i })).toHaveValue(settings.descr); + expect(screen.getByRole('textbox', { name: /url template/i })).toHaveValue(settings.url); + expect(screen.getByRole('textbox', { name: /query template/i })).toHaveValue(settings.query); + }) + + it('should fill inputs with right values, when engine!==custom', () => { + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: defaultEngine.id, descr: 'Some Descr', query: 'Some Query', url: 'Some Url' }); + setupSettingsSut(settingsEditorComp, settings); + + expect(screen.getByRole('combobox', { name: /query engine/i })).toHaveValue(settings.engine); + expect(screen.getByRole('textbox', { name: /description/i })).toHaveValue(defaultEngine.descr); + expect(screen.getByRole('textbox', { name: /url template/i })).toHaveValue(defaultEngine.url); + expect(screen.getByRole('textbox', { name: /query template/i })).toHaveValue(settings.query); + }) + + it('should enable descr/url inputs, when engine==custom', () => { + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: '' }); + setupSettingsSut(settingsEditorComp, settings); + + expect(screen.getByRole('textbox', { name: /description/i })).toBeEnabled(); + expect(screen.getByRole('textbox', { name: /url template/i })).toBeEnabled(); + }) + + it('should disable descr/url inputs, when engine!==custom', () => { + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: defaultEngine.id }); + setupSettingsSut(settingsEditorComp, settings); + + expect(screen.getByRole('textbox', { name: /description/i })).toBeDisabled(); + expect(screen.getByRole('textbox', { name: /url template/i })).toBeDisabled(); + }) + + it('should allow to update "engine" setting with an option select', async () => { + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: defaultEngine.id }); + const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); + const select = screen.getByRole('combobox', { name: /query engine/i }) + + userEvent.selectOptions(select, 'goog'); + await waitFor(() => expect((screen.getByRole('option', { name: 'Google' }) as HTMLOptionElement).selected).toBe(true)) + expect(getSettings()).toEqual({ + ...settings, + engine: 'goog' + }); + }) + + it('should update descr/url with current engine\'s values, when switching "engine" setting from non-custom to custom', async () => { + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: defaultEngine.id }); + const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); + const select = screen.getByRole('combobox', { name: /query engine/i }) + + userEvent.selectOptions(select, ''); + await waitFor(() => expect((screen.getByRole('option', { name: 'Custom Engine' }) as HTMLOptionElement).selected).toBe(true)) + expect(getSettings()).toEqual({ + ...settings, + engine: '', + descr: defaultEngine.descr, + url: defaultEngine.url + }); + }) + + it('should not update descr/url, when switching "engine" setting from custom to non-custom', async () => { + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: '' }); + const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); + const select = screen.getByRole('combobox', { name: /query engine/i }) + + userEvent.selectOptions(select, defaultEngine.id); + await waitFor(() => expect((screen.getByRole('option', { name: defaultEngine.name }) as HTMLOptionElement).selected).toBe(true)) + expect(getSettings()).toEqual({ + ...settings, + engine: defaultEngine.id, + }); + }) + + it('should allow to update "descr" setting with a text input', async () => { + const descr = 'descr'; + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: '', descr }); + const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); + const input = screen.getByRole('textbox', { name: /description/i }) + + await userEvent.type(input, '!'); + + expect(getSettings()).toEqual({ + ...settings, + descr: descr + '!' + }); + }) + + it('should allow to update "url" setting with a text input', async () => { + const url = 'url'; + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: '', url }); + const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); + const input = screen.getByRole('textbox', { name: /url template/i }) + + await userEvent.type(input, '!'); + + expect(getSettings()).toEqual({ + ...settings, + url: url + '!' + }); + }) + + it('should allow to update "query" setting with a text input', async () => { + const query = 'query'; + const settings = fixtureSettings({ mode: SettingsMode.Browser, engine: '', query }); + const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); + const input = screen.getByRole('textbox', { name: /query template/i }) + + await userEvent.type(input, '!'); + + expect(getSettings()).toEqual({ + ...settings, + query: query + '!' + }); + }) }) - it('should allow to update "descr" setting with a text input', async () => { - const descr = 'descr'; - const settings = fixtureSettings({ engine: '', descr }); - const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); - const input = screen.getByRole('textbox', { name: /description/i }) - - await userEvent.type(input, '!'); - - expect(getSettings()).toEqual({ - ...settings, - descr: descr + '!' - }); + describe('Webpages mode', () => { + it('should show right inputs', () => { + const settings = fixtureSettings({ mode: SettingsMode.Webpages }); + setupSettingsSut(settingsEditorComp, settings); + + expect(screen.queryByRole('combobox', { name: /query engine/i })).not.toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /description/i })).toBeInTheDocument(); + expect(screen.queryByRole('textbox', { name: /url template/i })).not.toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /query template/i })).toBeInTheDocument(); + }) + + it('should fill inputs with right values', () => { + const settings = fixtureSettings({ mode: SettingsMode.Webpages, descr: 'Some Descr', query: 'Some Query' }); + setupSettingsSut(settingsEditorComp, settings); + + expect(screen.getByRole('textbox', { name: /description/i })).toHaveValue(settings.descr); + expect(screen.getByRole('textbox', { name: /query template/i })).toHaveValue(settings.query); + }) + + it('should enable descr input (even when engine!==custom)', () => { + const settings = fixtureSettings({ mode: SettingsMode.Webpages, engine: defaultEngine.id }); + setupSettingsSut(settingsEditorComp, settings); + + expect(screen.getByRole('textbox', { name: /description/i })).toBeEnabled(); + }) + + it('should allow to update "descr" setting with a text input', async () => { + const descr = 'descr'; + const settings = fixtureSettings({ mode: SettingsMode.Webpages, descr }); + const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); + const input = screen.getByRole('textbox', { name: /description/i }) + + await userEvent.type(input, '!'); + + expect(getSettings()).toEqual({ + ...settings, + descr: descr + '!' + }); + }) + + it('should allow to update "query" setting with a text input', async () => { + const query = 'query'; + const settings = fixtureSettings({ mode: SettingsMode.Webpages, query }); + const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); + const input = screen.getByRole('textbox', { name: /query template/i }) + + await userEvent.type(input, '!'); + + expect(getSettings()).toEqual({ + ...settings, + query: query + '!' + }); + }) }) - - it('should allow to update "url" setting with a text input', async () => { - const url = 'url'; - const settings = fixtureSettings({ engine: '', url }); - const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); - const input = screen.getByRole('textbox', { name: /url template/i }) - - await userEvent.type(input, '!'); - - expect(getSettings()).toEqual({ - ...settings, - url: url + '!' - }); - }) - - it('should allow to update "query" setting with a text input', async () => { - const query = 'query'; - const settings = fixtureSettings({ engine: '', query }); - const { userEvent, getSettings } = setupSettingsSut(settingsEditorComp, settings); - const input = screen.getByRole('textbox', { name: /query template/i }) - - await userEvent.type(input, '!'); - - expect(getSettings()).toEqual({ - ...settings, - query: query + '!' - }); - }) - }) diff --git a/tests/renderer/widgets/web-query/widget.spec.ts b/tests/renderer/widgets/web-query/widget.spec.ts index efb3b50..263adce 100644 --- a/tests/renderer/widgets/web-query/widget.spec.ts +++ b/tests/renderer/widgets/web-query/widget.spec.ts @@ -3,11 +3,13 @@ * GNU General Public License v3.0 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) */ -import { Settings, defaultEngine } from '@/widgets/web-query/settings'; +import { Settings, SettingsMode, defaultEngine } from '@/widgets/web-query/settings'; import { widgetComp } from '@/widgets/web-query/widget' import { screen } from '@testing-library/react'; import { SetupWidgetSutOptional, setupWidgetSut } from '@tests/widgets/setupSut' import { fixtureSettings } from './fixtures'; +import { WebpageExposedApi } from '@/widgets/interfaces'; +import { WidgetApiWidget } from '@/widgets/appModules'; function setupSut(settings: Settings, optional?: SetupWidgetSutOptional) { const { comp, ...rest } = setupWidgetSut(widgetComp, settings, optional); @@ -18,166 +20,491 @@ function setupSut(settings: Settings, optional?: SetupWidgetSutOptional) { } describe('Web Query Widget', () => { - it('should render an "Invalid URL template" note, if engine=custom and url is empty', () => { - setupSut(fixtureSettings({ engine: '', url: '' })); + describe('Browser mode', () => { + it('should render an "Invalid URL template" note, if engine=custom and url is empty', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: '', url: '' })); - expect(screen.getByText(/Invalid URL template/i)).toBeInTheDocument(); - }) + expect(screen.getByText(/Invalid URL template/i)).toBeInTheDocument(); + }) - it('should render an "Invalid URL template" note, if engine=custom and url is invalid', () => { - setupSut(fixtureSettings({ engine: '', url: 'invalid^url/QUERY' })); + it('should render an "Invalid URL template" note, if engine=custom and url is invalid', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: '', url: 'invalid^url/QUERY' })); - expect(screen.getByText(/Invalid URL template/i)).toBeInTheDocument(); - }) + expect(screen.getByText(/Invalid URL template/i)).toBeInTheDocument(); + }) - it('should not render an "Invalid URL template" note, if engine!=custom and url is invalid', () => { - setupSut(fixtureSettings({ engine: defaultEngine.id, url: 'invalid^url/QUERY' })); + it('should not render an "Invalid URL template" note, if engine!=custom and url is invalid', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: defaultEngine.id, url: 'invalid^url/QUERY' })); - expect(screen.queryByText(/Invalid URL template/i)).not.toBeInTheDocument(); - }) + expect(screen.queryByText(/Invalid URL template/i)).not.toBeInTheDocument(); + }) - it('should render a "Missing QUERY in URL template" note, if engine=custom and url does not have QUERY', () => { - setupSut(fixtureSettings({ engine: '', url: 'https://freeter.io/' })); + it('should render a "Missing QUERY in URL template" note, if engine=custom and url does not have QUERY', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: '', url: 'https://freeter.io/' })); - expect(screen.getByText(/Missing QUERY in URL template/i)).toBeInTheDocument(); - }) + expect(screen.getByText(/Missing QUERY in URL template/i)).toBeInTheDocument(); + }) - it('should not render a "Missing QUERY in URL template" note, if engine!=custom and url does not have QUERY', () => { - setupSut(fixtureSettings({ engine: defaultEngine.id, url: 'https://freeter.io/' })); + it('should not render a "Missing QUERY in URL template" note, if engine!=custom and url does not have QUERY', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: defaultEngine.id, url: 'https://freeter.io/' })); - expect(screen.queryByText(/Missing QUERY in URL template/i)).not.toBeInTheDocument(); - }) + expect(screen.queryByText(/Missing QUERY in URL template/i)).not.toBeInTheDocument(); + }) - it('should render a "Missing QUERY in Query template" note, if query does not have QUERY', () => { - setupSut(fixtureSettings({ engine: '', url: 'https://freeter.io/QUERY', query: 'non-uppercase-query' })); + it('should render a "Missing QUERY in Query template" note, if query does not have QUERY', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: '', url: 'https://freeter.io/QUERY', query: 'non-uppercase-query' })); - expect(screen.getByText(/Missing QUERY in Query template/i)).toBeInTheDocument(); - }) + expect(screen.getByText(/Missing QUERY in Query template/i)).toBeInTheDocument(); + }) - it('should not render a "Missing QUERY in Query template" note, if query is empty', () => { - setupSut(fixtureSettings({ engine: '', url: 'https://freeter.io/QUERY', query: '' })); + it('should not render a "Missing QUERY in Query template" note, if query is empty', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: '', url: 'https://freeter.io/QUERY', query: '' })); - expect(screen.queryByText(/Missing QUERY in Query template/i)).not.toBeInTheDocument(); - }) + expect(screen.queryByText(/Missing QUERY in Query template/i)).not.toBeInTheDocument(); + }) - it('should render a text input and a button, when there are not any warning notes', () => { - setupSut(fixtureSettings({ engine: '', url: 'https://freeter.io/QUERY', query: '' })); + it('should render a text input and a button, when there are not any warning notes', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: '', url: 'https://freeter.io/QUERY', query: '' })); - expect(screen.getByRole('textbox')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /query/i })).toBeInTheDocument(); - }) + expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /query/i })).toBeInTheDocument(); + }) - it('should not render a text input and a button, when there is a warning notes', () => { - setupSut(fixtureSettings({ engine: '', url: '', query: '' })); + it('should not render a text input and a button, when there is a warning notes', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: '', url: '', query: '' })); - expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); - expect(screen.queryByRole('button', { name: /query/i })).not.toBeInTheDocument(); - }) + expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /query/i })).not.toBeInTheDocument(); + }) - it('should set the descr setting value as a text input placeholder, when engine=custom', () => { - const descr = 'Descr'; - setupSut(fixtureSettings({ engine: '', url: 'https://freeter.io/QUERY', query: '', descr })); + it('should set the descr setting value as a text input placeholder, when engine=custom', () => { + const descr = 'Descr'; + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: '', url: 'https://freeter.io/QUERY', query: '', descr })); - expect(screen.getByRole('textbox')).toHaveProperty('placeholder', descr); - }) + expect(screen.getByRole('textbox')).toHaveProperty('placeholder', descr); + }) - it('should set the engine descr as a text input placeholder, when engine!=custom', () => { - setupSut(fixtureSettings({ engine: 'ovrs', query: '', descr: 'Descr' })); + it('should set the engine descr as a text input placeholder, when engine!=custom', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: 'ovrs', query: '', descr: 'Descr' })); - expect(screen.getByRole('textbox')).toHaveProperty('placeholder', 'Search for content'); - }) + expect(screen.getByRole('textbox')).toHaveProperty('placeholder', 'Search for content'); + }) - it('should set the default engine descr as a text input placeholder, when engine does not exist', () => { - setupSut(fixtureSettings({ engine: 'NO-SUCH-ID', query: '', descr: 'Descr' })); + it('should set the default engine descr as a text input placeholder, when engine does not exist', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: 'NO-SUCH-ID', query: '', descr: 'Descr' })); - expect(screen.getByRole('textbox')).toHaveProperty('placeholder', defaultEngine.descr); - }) + expect(screen.getByRole('textbox')).toHaveProperty('placeholder', defaultEngine.descr); + }) - it('should call openExternalUrl with right args on ENTER keypress in the text input', async () => { - const someQuery = 'some query'; - const openExternalUrl = jest.fn(); - const { userEvent } = setupSut( - fixtureSettings({ engine: 'goog', query: 'query QUERY', url: 'https://freeter.io/QUERY' }), - { - mockWidgetApi: { - shell: { - openExternalUrl + it('should call openExternalUrl with right args on ENTER keypress in the text input', async () => { + const someQuery = 'some query'; + const openExternalUrl = jest.fn(); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Browser, engine: 'goog', query: 'query QUERY', url: 'https://freeter.io/QUERY' }), + { + mockWidgetApi: { + shell: { + openExternalUrl + } } } - } - ); - const textbox = screen.getByRole('textbox'); + ); + const textbox = screen.getByRole('textbox'); + + expect(openExternalUrl).not.toHaveBeenCalled(); + + await userEvent.type(textbox, someQuery + '[enter]'); + + expect(openExternalUrl).toHaveBeenCalledTimes(1); + expect(openExternalUrl).toHaveBeenCalledWith('https://www.google.com/search?q=query%20some%20query'); + }) + + it('should call openExternalUrl with right args on button press', async () => { + const someQuery = 'some query'; + const openExternalUrl = jest.fn(); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Browser, engine: 'goog', query: 'query QUERY', url: 'https://freeter.io/QUERY' }), + { + mockWidgetApi: { + shell: { + openExternalUrl + } + } + } + ); + const textbox = screen.getByRole('textbox'); + const button = screen.getByRole('button', { name: /query/i }); + await userEvent.type(textbox, someQuery); + + expect(openExternalUrl).not.toHaveBeenCalled(); + + await userEvent.click(button); + + expect(openExternalUrl).toHaveBeenCalledTimes(1); + expect(openExternalUrl).toHaveBeenCalledWith('https://www.google.com/search?q=query%20some%20query'); + }) + + it('should not request info about Webpage widgets on ENTER keypress in the text input', async () => { + const someQuery = 'some query'; + const webpageWidgets: WidgetApiWidget[] = [{ + id: '1', + name: 'name', + api: {} + }] + const getWidgetsInCurrentWorkflow = jest.fn().mockReturnValue(webpageWidgets); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Browser, engine: 'goog', query: 'query QUERY', url: 'https://freeter.io/QUERY' }), + { + mockWidgetApi: { + widgets: { + getWidgetsInCurrentWorkflow + } + } + } + ); + const textbox = screen.getByRole('textbox'); + await userEvent.type(textbox, someQuery + '[enter]'); + + expect(getWidgetsInCurrentWorkflow).not.toHaveBeenCalled(); + }) + + it('should not request info about Webpage widgets on button press', async () => { + const someQuery = 'some query'; + const webpageWidgets: WidgetApiWidget[] = [{ + id: '1', + name: 'name', + api: {} + }] + const getWidgetsInCurrentWorkflow = jest.fn().mockReturnValue(webpageWidgets); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Browser, engine: 'goog', query: 'query QUERY', url: 'https://freeter.io/QUERY' }), + { + mockWidgetApi: { + widgets: { + getWidgetsInCurrentWorkflow + } + } + } + ); + const textbox = screen.getByRole('textbox'); + const button = screen.getByRole('button', { name: /query/i }); + + await userEvent.type(textbox, someQuery); + await userEvent.click(button); + + expect(getWidgetsInCurrentWorkflow).not.toHaveBeenCalled(); + }) + + it('should build a right url, when engine=custom', async () => { + const someQuery = 'some query'; + const openExternalUrl = jest.fn(); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Browser, engine: '', query: 'query QUERY', url: 'freeter.io/QUERY' }), + { + mockWidgetApi: { + shell: { + openExternalUrl + } + } + } + ); + const textbox = screen.getByRole('textbox'); + + await userEvent.type(textbox, someQuery + '[enter]'); + + expect(openExternalUrl).toHaveBeenCalledWith('https://freeter.io/query%20some%20query'); + }) + + it('should build a right url, when engine!=custom', async () => { + const someQuery = 'some query'; + const openExternalUrl = jest.fn(); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Browser, engine: 'goog', query: 'query QUERY', url: 'freeter.io/QUERY' }), + { + mockWidgetApi: { + shell: { + openExternalUrl + } + } + } + ); + const textbox = screen.getByRole('textbox'); - expect(openExternalUrl).not.toHaveBeenCalled(); + await userEvent.type(textbox, someQuery + '[enter]'); - await userEvent.type(textbox, someQuery + '[enter]'); + expect(openExternalUrl).toHaveBeenCalledWith('https://www.google.com/search?q=query%20some%20query'); + }) - expect(openExternalUrl).toHaveBeenCalledTimes(1); - expect(openExternalUrl).toHaveBeenCalledWith('https://www.google.com/search?q=query%20some%20query'); }) - it('should call openExternalUrl with right args on button press', async () => { - const someQuery = 'some query'; - const openExternalUrl = jest.fn(); - const { userEvent } = setupSut( - fixtureSettings({ engine: 'goog', query: 'query QUERY', url: 'https://freeter.io/QUERY' }), - { - mockWidgetApi: { - shell: { - openExternalUrl - } - } - } - ); - const textbox = screen.getByRole('textbox'); - const button = screen.getByRole('button', { name: /query/i }); - await userEvent.type(textbox, someQuery); + describe('Webpages mode', () => { + it('should not render an "Invalid URL template" note, if engine=custom and url is empty', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Webpages, engine: '', url: '' })); - expect(openExternalUrl).not.toHaveBeenCalled(); + expect(screen.queryByText(/Invalid URL template/i)).not.toBeInTheDocument(); + }) - await userEvent.click(button); + it('should not render an "Invalid URL template" note, if engine=custom and url is invalid', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Webpages, engine: '', url: 'invalid^url/QUERY' })); - expect(openExternalUrl).toHaveBeenCalledTimes(1); - expect(openExternalUrl).toHaveBeenCalledWith('https://www.google.com/search?q=query%20some%20query'); - }) + expect(screen.queryByText(/Invalid URL template/i)).not.toBeInTheDocument(); + }) - it('should build a right url, when engine=custom', async () => { - const someQuery = 'some query'; - const openExternalUrl = jest.fn(); - const { userEvent } = setupSut( - fixtureSettings({ engine: '', query: 'query QUERY', url: 'freeter.io/QUERY' }), - { - mockWidgetApi: { - shell: { - openExternalUrl - } - } - } - ); - const textbox = screen.getByRole('textbox'); + it('should not render a "Missing QUERY in URL template" note, if engine=custom and url does not have QUERY', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Webpages, engine: '', url: 'https://freeter.io/' })); - await userEvent.type(textbox, someQuery + '[enter]'); + expect(screen.queryByText(/Missing QUERY in URL template/i)).not.toBeInTheDocument(); + }) - expect(openExternalUrl).toHaveBeenCalledWith('https://freeter.io/query%20some%20query'); - }) + it('should render a "Missing QUERY in Query template" note, if query does not have QUERY', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Webpages, engine: '', url: 'https://freeter.io/QUERY', query: 'non-uppercase-query' })); + + expect(screen.getByText(/Missing QUERY in Query template/i)).toBeInTheDocument(); + }) - it('should build a right url, when engine!=custom', async () => { - const someQuery = 'some query'; - const openExternalUrl = jest.fn(); - const { userEvent } = setupSut( - fixtureSettings({ engine: 'goog', query: 'query QUERY', url: 'freeter.io/QUERY' }), - { - mockWidgetApi: { - shell: { - openExternalUrl + it('should not render a "Missing QUERY in Query template" note, if query is empty', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Webpages, engine: '', url: 'https://freeter.io/QUERY', query: '' })); + + expect(screen.queryByText(/Missing QUERY in Query template/i)).not.toBeInTheDocument(); + }) + + it('should render a text input and a button, when there are not any warning notes', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Webpages, engine: '', url: '', query: '' })); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /query/i })).toBeInTheDocument(); + }) + + it('should not render a text input and a button, when there is a warning note', () => { + setupSut(fixtureSettings({ mode: SettingsMode.Browser, engine: '', url: '', query: 'non-uppercase-query' })); + + expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /query/i })).not.toBeInTheDocument(); + }) + + it('should set the descr setting value as a text input placeholder, when engine=custom', () => { + const descr = 'Descr'; + setupSut(fixtureSettings({ mode: SettingsMode.Webpages, engine: '', url: '', query: '', descr })); + + expect(screen.getByRole('textbox')).toHaveProperty('placeholder', descr); + }) + + it('should set the descr setting value as a text input placeholder, when engine!=custom', () => { + const descr = 'Descr'; + setupSut(fixtureSettings({ mode: SettingsMode.Webpages, engine: 'ovrs', query: '', descr })); + + expect(screen.getByRole('textbox')).toHaveProperty('placeholder', descr); + }) + + it('should set the descr setting value as a text input placeholder, when engine does not exist', () => { + const descr = 'Descr'; + setupSut(fixtureSettings({ mode: SettingsMode.Webpages, engine: 'NO-SUCH-ID', query: '', descr })); + + expect(screen.getByRole('textbox')).toHaveProperty('placeholder', descr); + }) + + it('should not call openExternalUrl on ENTER keypress in the text input', async () => { + const someQuery = 'some query'; + const webpageWidgets: WidgetApiWidget[] = [{ + id: '1', + name: 'name', + api: {} + }] + const getWidgetsInCurrentWorkflow = jest.fn().mockReturnValue(webpageWidgets); + const openExternalUrl = jest.fn(); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Webpages, engine: 'goog', query: 'query QUERY', url: 'https://freeter.io/QUERY' }), + { + mockWidgetApi: { + shell: { + openExternalUrl + }, + widgets: { + getWidgetsInCurrentWorkflow + } + } + } + ); + const textbox = screen.getByRole('textbox'); + + await userEvent.type(textbox, someQuery + '[enter]'); + + expect(openExternalUrl).not.toHaveBeenCalled(); + }) + + it('should not call openExternalUrl on button press', async () => { + const someQuery = 'some query'; + const webpageWidgets: WidgetApiWidget[] = [{ + id: '1', + name: 'name', + api: {} + }] + const getWidgetsInCurrentWorkflow = jest.fn().mockReturnValue(webpageWidgets); + const openExternalUrl = jest.fn(); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Webpages, engine: 'goog', query: 'query QUERY', url: 'https://freeter.io/QUERY' }), + { + mockWidgetApi: { + shell: { + openExternalUrl + }, + widgets: { + getWidgetsInCurrentWorkflow + } + } + } + ); + const textbox = screen.getByRole('textbox'); + const button = screen.getByRole('button', { name: /query/i }); + await userEvent.type(textbox, someQuery); + + await userEvent.click(button); + + expect(openExternalUrl).not.toHaveBeenCalled(); + }) + + it('should correctly use api methods exposed by Webpage widgets on ENTER keypress in the text input', async () => { + const someQuery = 'some query'; + const webpageWidgets: WidgetApiWidget[] = [{ + id: '1', + name: 'name', + api: { + getUrl: jest.fn(() => 'https://freeter.io/QUERY'), + openUrl: jest.fn() + } + }] + const getWidgetsInCurrentWorkflow = jest.fn().mockReturnValue(webpageWidgets); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Webpages, engine: '', query: 'query QUERY', url: '' }), + { + mockWidgetApi: { + widgets: { + getWidgetsInCurrentWorkflow + } + } + } + ); + const textbox = screen.getByRole('textbox'); + + expect(getWidgetsInCurrentWorkflow).not.toHaveBeenCalled(); + + await userEvent.type(textbox, someQuery + '[enter]'); + + expect(getWidgetsInCurrentWorkflow).toHaveBeenCalledTimes(1); + expect(getWidgetsInCurrentWorkflow).toHaveBeenCalledWith('webpage'); + expect(webpageWidgets[0].api.openUrl).toHaveBeenCalledTimes(1); + expect(webpageWidgets[0].api.openUrl).toHaveBeenCalledWith('https://freeter.io/query%20some%20query'); + }) + + it('should correctly use api methods exposed by Webpage widgets on button press', async () => { + const someQuery = 'some query'; + const webpageWidgets: WidgetApiWidget[] = [{ + id: '1', + name: 'name', + api: { + getUrl: jest.fn(() => 'https://freeter.io/QUERY'), + openUrl: jest.fn() + } + }] + const getWidgetsInCurrentWorkflow = jest.fn().mockReturnValue(webpageWidgets); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Webpages, engine: '', query: 'query QUERY', url: '' }), + { + mockWidgetApi: { + widgets: { + getWidgetsInCurrentWorkflow + } + } + } + ); + const textbox = screen.getByRole('textbox'); + const button = screen.getByRole('button', { name: /query/i }); + await userEvent.type(textbox, someQuery); + + expect(getWidgetsInCurrentWorkflow).not.toHaveBeenCalled(); + + await userEvent.click(button); + + expect(getWidgetsInCurrentWorkflow).toHaveBeenCalledTimes(1); + expect(getWidgetsInCurrentWorkflow).toHaveBeenCalledWith('webpage'); + expect(webpageWidgets[0].api.openUrl).toHaveBeenCalledTimes(1); + expect(webpageWidgets[0].api.openUrl).toHaveBeenCalledWith('https://freeter.io/query%20some%20query'); + }) + + it('should do nothing, if webpage url does not have QUERY', async () => { + const webpageWidgets: WidgetApiWidget[] = [{ + id: '1', + name: 'name', + api: { + getUrl: jest.fn(() => 'https://freeter.io/query'), + openUrl: jest.fn() + } + }] + const getWidgetsInCurrentWorkflow = jest.fn().mockReturnValue(webpageWidgets); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Webpages, engine: '', query: 'query QUERY', url: '' }), + { + mockWidgetApi: { + widgets: { + getWidgetsInCurrentWorkflow + } + } + } + ); + const textbox = screen.getByRole('textbox'); + + await userEvent.type(textbox, 'some query' + '[enter]'); + + expect(webpageWidgets[0].api.openUrl).not.toHaveBeenCalled(); + }) + it('should correctly do things for multiple widgets', async () => { + const someQuery = 'some query'; + const webpageWidgets: WidgetApiWidget[] = [{ + id: '1', + name: 'name', + api: { + getUrl: jest.fn(() => 'https://freeter.io/QUERY/1'), + openUrl: jest.fn() + } + }, { + id: '2', + name: 'name', + api: {} + }, { + id: '3', + name: 'name', + api: { + getUrl: jest.fn(() => 'https://freeter.io/QUERY/2'), + openUrl: jest.fn() + } + }, { + id: '4', + name: 'name', + api: { + getUrl: jest.fn(() => 'https://freeter.io/query'), + openUrl: jest.fn() + } + }] + const getWidgetsInCurrentWorkflow = jest.fn().mockReturnValue(webpageWidgets); + const { userEvent } = setupSut( + fixtureSettings({ mode: SettingsMode.Webpages, engine: '', query: 'query QUERY', url: '' }), + { + mockWidgetApi: { + widgets: { + getWidgetsInCurrentWorkflow + } } } - } - ); - const textbox = screen.getByRole('textbox'); + ); + const textbox = screen.getByRole('textbox'); - await userEvent.type(textbox, someQuery + '[enter]'); + await userEvent.type(textbox, someQuery + '[enter]'); - expect(openExternalUrl).toHaveBeenCalledWith('https://www.google.com/search?q=query%20some%20query'); + expect(webpageWidgets[0].api.openUrl).toHaveBeenCalledTimes(1); + expect(webpageWidgets[0].api.openUrl).toHaveBeenCalledWith('https://freeter.io/query%20some%20query/1'); + expect(webpageWidgets[2].api.openUrl).toHaveBeenCalledTimes(1); + expect(webpageWidgets[2].api.openUrl).toHaveBeenCalledWith('https://freeter.io/query%20some%20query/2'); + expect(webpageWidgets[3].api.openUrl).not.toHaveBeenCalled(); + }) }) })