From e8ec79747537cb91761f4cb88ad74ff5ebea4529 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 22 Jan 2025 16:10:10 +0300 Subject: [PATCH 1/4] refactor(plugins): translate to ts --- lib/static/components/extension-point.jsx | 2 +- .../{load-plugin.js => load-plugin.ts} | 83 ++++++++++++++----- lib/static/modules/{plugins.js => plugins.ts} | 38 ++++++--- lib/static/modules/reducers/plugins.js | 2 +- 4 files changed, 92 insertions(+), 33 deletions(-) rename lib/static/modules/{load-plugin.js => load-plugin.ts} (52%) rename lib/static/modules/{plugins.js => plugins.ts} (54%) diff --git a/lib/static/components/extension-point.jsx b/lib/static/components/extension-point.jsx index 1a5410c81..743f96719 100644 --- a/lib/static/components/extension-point.jsx +++ b/lib/static/components/extension-point.jsx @@ -64,7 +64,7 @@ function getExtensionPointComponents(loadedPluginConfigs, pointName) { return loadedPluginConfigs .map(config => { try { - const PluginComponent = plugins.get(config.name, config.component); + const PluginComponent = plugins.getPluginField(config.name, config.component); return { PluginComponent, name, diff --git a/lib/static/modules/load-plugin.js b/lib/static/modules/load-plugin.ts similarity index 52% rename from lib/static/modules/load-plugin.js rename to lib/static/modules/load-plugin.ts index dd4592f19..4c962727d 100644 --- a/lib/static/modules/load-plugin.js +++ b/lib/static/modules/load-plugin.ts @@ -32,6 +32,32 @@ const whitelistedDeps = { } }; +type WhitelistedDeps = typeof whitelistedDeps; +type WhitelistedDepName = keyof WhitelistedDeps; +type WhitelistedDep = typeof whitelistedDeps[WhitelistedDepName]; + +// Branded string +type ScriptText = string & {__script_text__: never}; + +type UnknownRecord = {[key: string]: unknown} +export type InstalledPlugin = UnknownRecord +export type PluginConfig = UnknownRecord + +interface PluginOptions { + pluginName: string; + pluginConfig: PluginConfig; + actions: typeof import('./actions'); + actionNames: typeof actionNames; + selectors: typeof selectors; +} + +type PluginFunction = (args: [...WhitelistedDep[], PluginOptions]) => InstalledPlugin + +type ModuleWithDefaultFunction = {default: PluginFunction}; + +type CompiledPluginWithDeps = [...WhitelistedDepName[], PluginFunction]; +type CompiledPlugin = InstalledPlugin | ModuleWithDefaultFunction | PluginFunction | CompiledPluginWithDeps + // It's expected that in the plugin code there is a // __testplane_html_reporter_register_plugin__ call, // with actual plugin passed. @@ -49,16 +75,13 @@ const whitelistedDeps = { // - an array with the string list of required dependencies and a function as the last item. // The function will be called with the dependencies as arguments plus `options` arg. -const loadingPlugins = {}; -const pendingPlugins = {}; - -const getPluginScriptPath = pluginName => `plugins/${encodeURIComponent(pluginName)}/plugin.js`; - -export function preloadPlugin(pluginName) { +const loadingPlugins: Record | undefined> = {}; +const pendingPlugins: Record | undefined> = {}; +export function preloadPlugin(pluginName: string): void { loadingPlugins[pluginName] = loadingPlugins[pluginName] || getScriptText(pluginName); } -export async function loadPlugin(pluginName, pluginConfig) { +export async function loadPlugin(pluginName: string, pluginConfig: PluginConfig): Promise { if (pendingPlugins[pluginName]) { return pendingPlugins[pluginName]; } @@ -66,55 +89,73 @@ export async function loadPlugin(pluginName, pluginConfig) { const scriptTextPromise = loadingPlugins[pluginName] || getScriptText(pluginName); return pendingPlugins[pluginName] = scriptTextPromise - .then(executePluginCode) + .then(compilePlugin) .then(plugin => initPlugin(plugin, pluginName, pluginConfig)) - .then(null, err => { + .catch(err => { console.error(`Plugin "${pluginName}" failed to load.`, err); - return null; + return undefined; }); } -async function initPlugin(plugin, pluginName, pluginConfig) { +const hasDefault = (plugin: CompiledPlugin): plugin is ModuleWithDefaultFunction => + _.isObject(plugin) && !_.isArray(plugin) && !_.isFunction(plugin) && _.isFunction(plugin.default); + +const getDeps = (pluginWithDeps: CompiledPluginWithDeps): WhitelistedDepName[] => pluginWithDeps.slice(0, -1) as WhitelistedDepName[]; +const getPluginFn = (pluginWithDeps: CompiledPluginWithDeps): PluginFunction => _.last(pluginWithDeps) as PluginFunction; + +async function initPlugin(plugin: CompiledPlugin, pluginName: string, pluginConfig: PluginConfig): Promise { try { if (!_.isObject(plugin)) { - return null; + return undefined; } - plugin = plugin.default || plugin; + plugin = hasDefault(plugin) ? plugin.default : plugin; if (typeof plugin === 'function') { plugin = [plugin]; } if (Array.isArray(plugin)) { - const deps = plugin.slice(0, -1); - plugin = _.last(plugin); + const deps = getDeps(plugin); + + const pluginFn = getPluginFn(plugin); + const depArgs = deps.map(dep => whitelistedDeps[dep]); + // cyclic dep, resolve it dynamically const actions = await import('./actions'); - return plugin(...depArgs, {pluginName, pluginConfig, actions, actionNames, selectors}); + + // @ts-expect-error Unfortunately, for historical reasons + // the order of arguments and their types are not amenable to normal typing, so we will have to ignore the error here. + return pluginFn(...depArgs, {pluginName, pluginConfig, actions, actionNames, selectors}); } return plugin; } catch (err) { console.error(`Error on "${pluginName}" plugin initialization:`, err); - return null; + return undefined; } } // Actual plugin is passed to __testplane_html_reporter_register_plugin__ somewhere in the // plugin code -function executePluginCode(code) { - const getRegisterFn = tool => `function __${tool}_html_reporter_register_plugin__(p) {return p;};`; - +function compilePlugin(code: ScriptText): CompiledPlugin { const exec = new Function(`${getRegisterFn('testplane')} ${getRegisterFn('hermione')} return ${code};`); return exec(); } -async function getScriptText(pluginName) { +async function getScriptText(pluginName: string): Promise { const scriptUrl = getPluginScriptPath(pluginName); const {data} = await axios.get(scriptUrl); return data; } + +function getRegisterFn(tool: string): string { + return `function __${tool}_html_reporter_register_plugin__(p) {return p;};`; +} + +function getPluginScriptPath(pluginName: string): string { + return `plugins/${encodeURIComponent(pluginName)}/plugin.js`; +} diff --git a/lib/static/modules/plugins.js b/lib/static/modules/plugins.ts similarity index 54% rename from lib/static/modules/plugins.js rename to lib/static/modules/plugins.ts index 70b30e08f..aa7ee214c 100644 --- a/lib/static/modules/plugins.js +++ b/lib/static/modules/plugins.ts @@ -1,9 +1,19 @@ -import {loadPlugin, preloadPlugin} from './load-plugin'; +import {InstalledPlugin, loadPlugin, PluginConfig, preloadPlugin} from './load-plugin'; -const plugins = Object.create(null); -const loadedPluginConfigs = []; +interface PluginSetupInfo { + name: string + config: PluginConfig +} + +interface Config { + pluginsEnabled: boolean; + plugins: PluginSetupInfo[] +} + +const plugins: Record = {}; +const loadedPluginConfigs: PluginSetupInfo[] = []; -export function preloadAll(config) { +export function preloadAll(config: Config): void { if (!config || !config.pluginsEnabled || !Array.isArray(config.plugins)) { return; } @@ -11,13 +21,13 @@ export function preloadAll(config) { config.plugins.forEach(plugin => preloadPlugin(plugin.name)); } -export async function loadAll(config) { +export async function loadAll(config: Config): Promise { // if plugins are disabled, act like there are no plugins defined if (!config || !config.pluginsEnabled || !Array.isArray(config.plugins)) { return; } - const pluginConfigs = await Promise.all(config.plugins.map(async pluginConfig => { + const pluginsSetupInfo = await Promise.all(config.plugins.map(async pluginConfig => { const plugin = await loadPlugin(pluginConfig.name, pluginConfig.config); if (plugin) { plugins[pluginConfig.name] = plugin; @@ -25,10 +35,18 @@ export async function loadAll(config) { } })); - loadedPluginConfigs.push(...pluginConfigs.filter(Boolean)); + pluginsSetupInfo.map((setupInfo) => { + if (!setupInfo) { + return; + } + + loadedPluginConfigs.push(setupInfo); + }); } -export function forEach(callback) { +type ForEachPluginCallback = (plugin: InstalledPlugin, name: string) => void; + +export function forEachPlugin(callback: ForEachPluginCallback): void { const visited = new Set(); loadedPluginConfigs.forEach(({name}) => { if (!visited.has(name)) { @@ -42,7 +60,7 @@ export function forEach(callback) { }); } -export function get(name, field) { +export function getPluginField(name: string, field: string): unknown { const plugin = plugins[name]; if (!plugin) { throw new Error(`Plugin "${name}" is not loaded.`); @@ -53,6 +71,6 @@ export function get(name, field) { return plugins[name][field]; } -export function getLoadedConfigs() { +export function getLoadedConfigs(): PluginSetupInfo[] { return loadedPluginConfigs; } diff --git a/lib/static/modules/reducers/plugins.js b/lib/static/modules/reducers/plugins.js index 61effa9de..de8327cd1 100644 --- a/lib/static/modules/reducers/plugins.js +++ b/lib/static/modules/reducers/plugins.js @@ -11,7 +11,7 @@ export default function(state, action) { case actionNames.INIT_STATIC_REPORT: { const pluginReducers = []; - plugins.forEach(plugin => { + plugins.forEachPlugin(plugin => { if (Array.isArray(plugin.reducers)) { pluginReducers.push(reduceReducers(state, ...plugin.reducers)); } From 815f7ae5762d514bc66ce7b3cc7892549c396cde Mon Sep 17 00:00:00 2001 From: = <=> Date: Fri, 24 Jan 2025 10:20:19 +0300 Subject: [PATCH 2/4] refactor(extension-point): translate to ts --- lib/static/components/extension-point.jsx | 107 -------------- lib/static/components/extension-point.tsx | 136 ++++++++++++++++++ lib/static/modules/load-plugin.ts | 15 +- lib/static/modules/plugins.ts | 37 +++-- lib/types.ts | 40 +++++- .../lib/static/components/extension-point.jsx | 2 +- test/unit/lib/static/modules/plugins.js | 14 +- .../lib/static/modules/reducers/plugins.js | 2 +- 8 files changed, 209 insertions(+), 144 deletions(-) delete mode 100644 lib/static/components/extension-point.jsx create mode 100644 lib/static/components/extension-point.tsx diff --git a/lib/static/components/extension-point.jsx b/lib/static/components/extension-point.jsx deleted file mode 100644 index 743f96719..000000000 --- a/lib/static/components/extension-point.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, {Component, Fragment} from 'react'; -import PropTypes from 'prop-types'; -import ErrorBoundary from './error-boundary'; -import * as plugins from '../modules/plugins'; - -export default class ExtensionPoint extends Component { - static propTypes = { - name: PropTypes.string.isRequired, - children: PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.array]) - }; - - render() { - const loadedPluginConfigs = plugins.getLoadedConfigs(); - - if (loadedPluginConfigs.length) { - const {name: pointName, children: reportComponent, ...componentProps} = this.props; - const pluginComponents = getExtensionPointComponents(loadedPluginConfigs, pointName); - return getComponentsComposition(pluginComponents, reportComponent, componentProps); - } - - return this.props.children; - } -} - -function getComponentsComposition(pluginComponents, reportComponent, componentProps) { - let currentComponent = reportComponent; - - for (const {PluginComponent, position, config} of pluginComponents) { - currentComponent = composeComponents(PluginComponent, componentProps, currentComponent, position, config); - } - - return currentComponent; -} - -function composeComponents(PluginComponent, pluginProps, currentComponent, position, config) { - switch (position) { - case 'wrap': - return - - {currentComponent} - - ; - case 'before': - return - - - - {currentComponent} - ; - case 'after': - return - {currentComponent} - - - - ; - default: - console.error(`${getComponentSpec(config)} unexpected position "${position}" specified.`); - return currentComponent; - } -} - -function getExtensionPointComponents(loadedPluginConfigs, pointName) { - return loadedPluginConfigs - .map(config => { - try { - const PluginComponent = plugins.getPluginField(config.name, config.component); - return { - PluginComponent, - name, - point: getComponentPoint(PluginComponent, config), - position: getComponentPosition(PluginComponent, config), - config - }; - } catch (err) { - console.error(err); - return {}; - } - }) - .filter(({point, position}) => { - return point && position && point === pointName; - }); -} - -function getComponentPoint(component, config) { - return getComponentConfigField(component, config, 'point'); -} - -function getComponentPosition(component, config) { - return getComponentConfigField(component, config, 'position'); -} - -function getComponentConfigField(component, config, field) { - if (component[field] && config[field] && component[field] !== config[field]) { - console.error(`${getComponentSpec(config)} "${field}" field does not match the one from the config: "${Component[field]}" vs "${config[field]}".`); - return null; - } else if (!component[field] && !config[field]) { - console.error(`${getComponentSpec(config)} "${field}" field is not set.`); - return null; - } - - return component[field] || config[field]; -} - -function getComponentSpec(config) { - return `Component "${config.component}" of "${config.name}" plugin`; -} diff --git a/lib/static/components/extension-point.tsx b/lib/static/components/extension-point.tsx new file mode 100644 index 000000000..684bee57d --- /dev/null +++ b/lib/static/components/extension-point.tsx @@ -0,0 +1,136 @@ +import React, {Component, FC, ReactNode} from 'react'; +import * as plugins from '../modules/plugins'; +import {PLUGIN_COMPONENT_POSITIONS, PluginComponentPosition, PluginDescription} from '@/types'; +import ErrorBoundary from './error-boundary'; + +interface ExtensionPointProps { + name: string; + children?: React.ReactNode; +} + +interface ExtensionPointComponent { + PluginComponent: FC; + name: string; + point: string; + position: PluginComponentPosition; + config: PluginDescription; +} + +type ExtensionPointComponentUnchecked = + Omit + & Partial> + +export default class ExtensionPoint extends Component { + render(): ReactNode { + const loadedPluginConfigs = plugins.getLoadedConfigs(); + + if (loadedPluginConfigs.length) { + const {name: pointName, children: reportComponent, ...componentProps} = this.props; + const pluginComponents = getExtensionPointComponents(loadedPluginConfigs, pointName); + return getComponentsComposition(pluginComponents, reportComponent, componentProps); + } + + return this.props.children; + } +} + +function getComponentsComposition(pluginComponents: ExtensionPointComponent[], reportComponent: ReactNode, componentProps: any): ReactNode { + let currentComponent = reportComponent; + + for (const {PluginComponent, position, config} of pluginComponents) { + currentComponent = composeComponents(PluginComponent, componentProps, currentComponent, position, config); + } + + return currentComponent; +} + +function composeComponents(PluginComponent: FC, pluginProps: any, currentComponent: ReactNode, position: PluginComponentPosition, config: PluginDescription): ReactNode { + switch (position) { + case 'wrap': + return + + {currentComponent} + + ; + case 'before': + return <> + + + + {currentComponent} + ; + case 'after': + return <> + {currentComponent} + + + + ; + default: + console.error(`${getComponentSpec(config)} unexpected position "${position}" specified.`); + return currentComponent; + } +} + +function getExtensionPointComponents(loadedPluginConfigs: PluginDescription[], pointName: string): ExtensionPointComponent[] { + return loadedPluginConfigs + .map(pluginDescription => { + try { + const PluginComponent = plugins.getPluginField(pluginDescription.name, pluginDescription.component); + + return { + PluginComponent, + name: pluginDescription.name, + point: getComponentPoint(PluginComponent, pluginDescription), + position: getComponentPosition(PluginComponent, pluginDescription), + config: pluginDescription + }; + } catch (err) { + console.error(err); + return {} as ExtensionPointComponentUnchecked; + } + }) + .filter((component: ExtensionPointComponentUnchecked): component is ExtensionPointComponent => { + return Boolean(component.point && component.position && component.point === pointName); + }); +} + +function getComponentPoint(component: FC, config: PluginDescription): string | undefined { + const result = getComponentConfigField(component, config, 'point'); + + if (typeof result !== 'string') { + return; + } + + return result as string; +} + +function getComponentPosition(component: FC, config: PluginDescription): PluginComponentPosition | undefined { + const result = getComponentConfigField(component, config, 'position'); + + if (typeof result !== 'string') { + return; + } + + if (!PLUGIN_COMPONENT_POSITIONS.includes(result as PluginComponentPosition)) { + return; + } + + return result as PluginComponentPosition; +} + +function getComponentConfigField(component: any, config: any, field: string): unknown | null { + if (component[field] && config[field] && component[field] !== config[field]) { + console.error(`${getComponentSpec(config)} "${field}" field does not match the one from the config: "${component[field]}" vs "${config[field]}".`); + return null; + } else if (!component[field] && !config[field]) { + console.error(`${getComponentSpec(config)} "${field}" field is not set.`); + return null; + } + + return component[field] || config[field]; +} + +function getComponentSpec(pluginDescription: PluginDescription): string { + return `Component "${pluginDescription.component}" of "${pluginDescription.name}" plugin`; +} diff --git a/lib/static/modules/load-plugin.ts b/lib/static/modules/load-plugin.ts index 4c962727d..f155f057c 100644 --- a/lib/static/modules/load-plugin.ts +++ b/lib/static/modules/load-plugin.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {Component, FC} from 'react'; import * as Redux from 'redux'; import * as ReactRedux from 'react-redux'; import _ from 'lodash'; @@ -13,6 +13,7 @@ import axios from 'axios'; import * as selectors from './selectors'; import actionNames from './action-names'; import Details from '../components/details'; +import {PluginConfig} from '@/types'; const whitelistedDeps = { 'react': React, @@ -39,9 +40,11 @@ type WhitelistedDep = typeof whitelistedDeps[WhitelistedDepName]; // Branded string type ScriptText = string & {__script_text__: never}; -type UnknownRecord = {[key: string]: unknown} -export type InstalledPlugin = UnknownRecord -export type PluginConfig = UnknownRecord +export type InstalledPlugin = { + name?: string; + component?: FC | Component; + [key: string]: unknown; +} interface PluginOptions { pluginName: string; @@ -81,7 +84,7 @@ export function preloadPlugin(pluginName: string): void { loadingPlugins[pluginName] = loadingPlugins[pluginName] || getScriptText(pluginName); } -export async function loadPlugin(pluginName: string, pluginConfig: PluginConfig): Promise { +export async function loadPlugin(pluginName: string, pluginConfig?: PluginConfig): Promise { if (pendingPlugins[pluginName]) { return pendingPlugins[pluginName]; } @@ -103,7 +106,7 @@ const hasDefault = (plugin: CompiledPlugin): plugin is ModuleWithDefaultFunction const getDeps = (pluginWithDeps: CompiledPluginWithDeps): WhitelistedDepName[] => pluginWithDeps.slice(0, -1) as WhitelistedDepName[]; const getPluginFn = (pluginWithDeps: CompiledPluginWithDeps): PluginFunction => _.last(pluginWithDeps) as PluginFunction; -async function initPlugin(plugin: CompiledPlugin, pluginName: string, pluginConfig: PluginConfig): Promise { +async function initPlugin(plugin: CompiledPlugin, pluginName: string, pluginConfig?: PluginConfig): Promise { try { if (!_.isObject(plugin)) { return undefined; diff --git a/lib/static/modules/plugins.ts b/lib/static/modules/plugins.ts index aa7ee214c..d5ea29a8f 100644 --- a/lib/static/modules/plugins.ts +++ b/lib/static/modules/plugins.ts @@ -1,19 +1,13 @@ -import {InstalledPlugin, loadPlugin, PluginConfig, preloadPlugin} from './load-plugin'; +import {ConfigForStaticFile} from '@/server-utils'; +import {InstalledPlugin, loadPlugin, preloadPlugin} from './load-plugin'; +import {PluginDescription} from '@/types'; -interface PluginSetupInfo { - name: string - config: PluginConfig -} - -interface Config { - pluginsEnabled: boolean; - plugins: PluginSetupInfo[] -} +export type ExtensionPointComponentPosition = 'wrap' | 'before' | 'after' const plugins: Record = {}; -const loadedPluginConfigs: PluginSetupInfo[] = []; +const loadedPluginConfigs: PluginDescription[] = []; -export function preloadAll(config: Config): void { +export function preloadAll(config?: ConfigForStaticFile): void { if (!config || !config.pluginsEnabled || !Array.isArray(config.plugins)) { return; } @@ -21,18 +15,21 @@ export function preloadAll(config: Config): void { config.plugins.forEach(plugin => preloadPlugin(plugin.name)); } -export async function loadAll(config: Config): Promise { +export async function loadAll(config?: ConfigForStaticFile): Promise { // if plugins are disabled, act like there are no plugins defined if (!config || !config.pluginsEnabled || !Array.isArray(config.plugins)) { return; } - const pluginsSetupInfo = await Promise.all(config.plugins.map(async pluginConfig => { - const plugin = await loadPlugin(pluginConfig.name, pluginConfig.config); + const pluginsSetupInfo = await Promise.all(config.plugins.map(async pluginDescription => { + const plugin = await loadPlugin(pluginDescription.name, pluginDescription.config); + if (plugin) { - plugins[pluginConfig.name] = plugin; - return pluginConfig; + plugins[pluginDescription.name] = plugin; + return pluginDescription; } + + return undefined; })); pluginsSetupInfo.map((setupInfo) => { @@ -60,7 +57,7 @@ export function forEachPlugin(callback: ForEachPluginCallback): void { }); } -export function getPluginField(name: string, field: string): unknown { +export function getPluginField(name: string, field: string): T { const plugin = plugins[name]; if (!plugin) { throw new Error(`Plugin "${name}" is not loaded.`); @@ -68,9 +65,9 @@ export function getPluginField(name: string, field: string): unknown { if (!plugin[field]) { throw new Error(`"${field}" is not defined on plugin "${name}".`); } - return plugins[name][field]; + return plugins[name][field] as T; } -export function getLoadedConfigs(): PluginSetupInfo[] { +export function getLoadedConfigs(): PluginDescription[] { return loadedPluginConfigs; } diff --git a/lib/types.ts b/lib/types.ts index 93d8282bf..bd2b1cfbf 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -171,12 +171,48 @@ export interface ErrorPattern { pattern: string; } +/** + * Plugin configuration. Passed directly to the plugin. + */ +export type PluginConfig = Record; + +export const PLUGIN_COMPONENT_POSITIONS = ['after', 'before', 'wrap'] as const; + +export type PluginComponentPosition = typeof PLUGIN_COMPONENT_POSITIONS[number]; + +/** + * Description of configuration parameters + * @link https://testplane.io/docs/v8/html-reporter/html-reporter-plugins/ + */ export interface PluginDescription { + /** + * The name of the package with the plugin for the report. + * It is assumed that the plugin can be connected using require(name). + */ name: string; + /** + * The name of the React component from the plugin. + */ component: string; + /** + * The name of the extension point in the html-reporter plugin. + */ point?: string; - position?: 'after' | 'before' | 'wrap'; - config?: Record; + /** + * Defines the method by which the component will be placed + * at the extension point of the html-report user interface. + * + * wrap: wrap the extension point in the UI + * + * before: place the component before the extension point + * + * after: place the component after the extension point + */ + position?: PluginComponentPosition; + /** + * Plugin configuration. Passed directly to the plugin. + */ + config?: PluginConfig } export interface CustomGuiItem { diff --git a/test/unit/lib/static/components/extension-point.jsx b/test/unit/lib/static/components/extension-point.jsx index 8fea3f51a..0fd922219 100644 --- a/test/unit/lib/static/components/extension-point.jsx +++ b/test/unit/lib/static/components/extension-point.jsx @@ -27,7 +27,7 @@ describe('', () => { }[component])); const pluginsStub = { - get: pluginsGetStub, + getPluginField: pluginsGetStub, getLoadedConfigs: () => [ {name: 'plugin', component: 'WrapComponent', point: 'example', position: 'wrap'}, {name: 'plugin', component: 'BeforeComponent', point: 'example', position: 'before'}, diff --git a/test/unit/lib/static/modules/plugins.js b/test/unit/lib/static/modules/plugins.js index 17b02ec28..bf3d9512b 100644 --- a/test/unit/lib/static/modules/plugins.js +++ b/test/unit/lib/static/modules/plugins.js @@ -125,14 +125,14 @@ describe('static/modules/plugins', () => { it('should throw when requested plugin is not loaded', async () => { await plugins.loadAll(); - assert.throws(() => plugins.get('plugin-x', 'TestComponent'), 'Plugin "plugin-x" is not loaded.'); + assert.throws(() => plugins.getPluginField('plugin-x', 'TestComponent'), 'Plugin "plugin-x" is not loaded.'); }); it('should throw when specified plugin component does not exist', async () => { loadPluginStub.resolves({}); await plugins.loadAll({pluginsEnabled: true, plugins: [{name: 'plugin-a'}]}); - assert.throws(() => plugins.get('plugin-a', 'TestComponent'), '"TestComponent" is not defined on plugin "plugin-a".'); + assert.throws(() => plugins.getPluginField('plugin-a', 'TestComponent'), '"TestComponent" is not defined on plugin "plugin-a".'); }); it('should return requested component when plugin is loaded', async () => { @@ -140,7 +140,7 @@ describe('static/modules/plugins', () => { loadPluginStub.resolves({TestComponent}); await plugins.loadAll({pluginsEnabled: true, plugins: [{name: 'plugin-a'}]}); - const result = plugins.get('plugin-a', 'TestComponent'); + const result = plugins.getPluginField('plugin-a', 'TestComponent'); assert.strictEqual(result, TestComponent); }); @@ -156,7 +156,7 @@ describe('static/modules/plugins', () => { it('should not call the callback when no plugins are loaded', async () => { await plugins.loadAll(); - plugins.forEach(callbackStub); + plugins.forEachPlugin(callbackStub); assert.notCalled(callbackStub); }); @@ -164,7 +164,7 @@ describe('static/modules/plugins', () => { it('should not call the callback when plugins are enabled but not defined', async () => { await plugins.loadAll({pluginsEnabled: true}); - plugins.forEach(callbackStub); + plugins.forEachPlugin(callbackStub); assert.notCalled(callbackStub); }); @@ -182,7 +182,7 @@ describe('static/modules/plugins', () => { ] }); - plugins.forEach(callbackStub); + plugins.forEachPlugin(callbackStub); assert.notCalled(callbackStub); }); @@ -207,7 +207,7 @@ describe('static/modules/plugins', () => { ] }); - plugins.forEach(callbackStub); + plugins.forEachPlugin(callbackStub); assert.deepStrictEqual(callbackStub.args, [ [pluginsToLoad['plugin-a'], 'plugin-a'], diff --git a/test/unit/lib/static/modules/reducers/plugins.js b/test/unit/lib/static/modules/reducers/plugins.js index dba8e67fa..b6d5ab317 100644 --- a/test/unit/lib/static/modules/reducers/plugins.js +++ b/test/unit/lib/static/modules/reducers/plugins.js @@ -10,7 +10,7 @@ describe('lib/static/modules/reducers/plugins', () => { beforeEach(() => { forEachStub = sandbox.stub(); reducer = proxyquire('lib/static/modules/reducers/plugins', { - '../plugins': {forEach: forEachStub} + '../plugins': {forEachPlugin: forEachStub} }).default; forEachStub.callsFake((callback) => callback({reducers: [ From 0196249e063a38f41fd70031a716f7b807250e1a Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 27 Jan 2025 13:38:18 +0300 Subject: [PATCH 3/4] refactor(plugins): add warning when has plugins and they are disabled --- lib/static/modules/plugins.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/static/modules/plugins.ts b/lib/static/modules/plugins.ts index d5ea29a8f..0e454f8c9 100644 --- a/lib/static/modules/plugins.ts +++ b/lib/static/modules/plugins.ts @@ -16,8 +16,16 @@ export function preloadAll(config?: ConfigForStaticFile): void { } export async function loadAll(config?: ConfigForStaticFile): Promise { + if (!config || !Array.isArray(config.plugins)) { + return; + } + // if plugins are disabled, act like there are no plugins defined - if (!config || !config.pluginsEnabled || !Array.isArray(config.plugins)) { + if (!config.pluginsEnabled) { + if (config.plugins.length > 0) { + console.warn(`HTML Reporter plugins are disabled, but there are ${config.plugins.length} plugins in the config. Please, check your testplane.config.ts file and set pluginsEnabled to true.`); + } + return; } From ff2d443e7d2b494733c2f5d84f09ebb403e30196 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 27 Jan 2025 13:39:39 +0300 Subject: [PATCH 4/4] fix(load-plugin): fix initialization --- lib/static/modules/load-plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/static/modules/load-plugin.ts b/lib/static/modules/load-plugin.ts index f155f057c..88a0ac344 100644 --- a/lib/static/modules/load-plugin.ts +++ b/lib/static/modules/load-plugin.ts @@ -101,7 +101,7 @@ export async function loadPlugin(pluginName: string, pluginConfig?: PluginConfig } const hasDefault = (plugin: CompiledPlugin): plugin is ModuleWithDefaultFunction => - _.isObject(plugin) && !_.isArray(plugin) && !_.isFunction(plugin) && _.isFunction(plugin.default); + _.isObject(plugin) && !_.isArray(plugin) && !_.isFunction(plugin) && (_.isFunction(plugin.default) || _.isArray(plugin.default)); const getDeps = (pluginWithDeps: CompiledPluginWithDeps): WhitelistedDepName[] => pluginWithDeps.slice(0, -1) as WhitelistedDepName[]; const getPluginFn = (pluginWithDeps: CompiledPluginWithDeps): PluginFunction => _.last(pluginWithDeps) as PluginFunction;