|
| 1 | +/// <reference path="../../@types/bstack-service-types.d.ts" /> |
| 2 | +import BaseModule from './baseModule.js' |
| 3 | +import { BStackLogger } from '../cliLogger.js' |
| 4 | +import TestFramework from '../frameworks/testFramework.js' |
| 5 | +import AutomationFramework from '../frameworks/automationFramework.js' |
| 6 | +import type AutomationFrameworkInstance from '../instances/automationFrameworkInstance.js' |
| 7 | +import type TestFrameworkInstance from '../instances/testFrameworkInstance.js' |
| 8 | +import { AutomationFrameworkState } from '../states/automationFrameworkState.js' |
| 9 | +import { HookState } from '../states/hookState.js' |
| 10 | +import { TestFrameworkConstants } from '../frameworks/constants/testFrameworkConstants.js' |
| 11 | +import { CLIUtils } from '../cliUtils.js' |
| 12 | +import WdioMochaTestFramework from '../frameworks/wdioMochaTestFramework.js' |
| 13 | +import { mergeIntoTags, parseCommaSeparatedValues } from '../../customTags.js' |
| 14 | +import type { CustomMetadata } from '../../customTags.js' |
| 15 | + |
| 16 | +/** |
| 17 | + * CustomTagsModule — CLI/gRPC path registration for `browser.setCustomTags`. |
| 18 | + * |
| 19 | + * Mirrors AccessibilityModule: registers the browser method in onBeforeExecute() |
| 20 | + * (observer-bound to AutomationFrameworkState.CREATE / HookState.POST), instantiated |
| 21 | + * from BrowserstackCLI.loadModules() whenever the binary is up. |
| 22 | + * |
| 23 | + * The per-test custom_metadata is merged directly into the tracked |
| 24 | + * TestFrameworkInstance map under KEY_CUSTOM_TAGS ('custom_metadata'). That whole |
| 25 | + * map is serialized verbatim into event_json (testHubModule.sendTestFrameworkEvent), |
| 26 | + * so the binary reads it as event.custom_metadata. The instance is created fresh |
| 27 | + * per test, so per-instance merge gives test-scoped union/dedupe; the binary is a |
| 28 | + * strict pass-through and does NOT consolidate across events. |
| 29 | + */ |
| 30 | +export default class CustomTagsModule extends BaseModule { |
| 31 | + |
| 32 | + logger = BStackLogger |
| 33 | + name: string |
| 34 | + static MODULE_NAME = 'CustomTagsModule' |
| 35 | + |
| 36 | + constructor() { |
| 37 | + super() |
| 38 | + this.name = 'CustomTagsModule' |
| 39 | + AutomationFramework.registerObserver(AutomationFrameworkState.CREATE, HookState.POST, this.onBeforeExecute.bind(this)) |
| 40 | + } |
| 41 | + |
| 42 | + getModuleName() { |
| 43 | + return CustomTagsModule.MODULE_NAME |
| 44 | + } |
| 45 | + |
| 46 | + async onBeforeExecute() { |
| 47 | + try { |
| 48 | + const autoInstance: AutomationFrameworkInstance = AutomationFramework.getTrackedInstance() |
| 49 | + if (!autoInstance) { |
| 50 | + this.logger.debug('CustomTagsModule: No tracked automation instance found!') |
| 51 | + return |
| 52 | + } |
| 53 | + |
| 54 | + const browser = AutomationFramework.getDriver(autoInstance) as WebdriverIO.Browser |
| 55 | + if (!browser) { |
| 56 | + this.logger.debug('CustomTagsModule: No browser instance found for setCustomTags registration') |
| 57 | + return |
| 58 | + } |
| 59 | + |
| 60 | + (browser as WebdriverIO.Browser).setCustomTags = async (key: string, value: string): Promise<void> => { |
| 61 | + try { |
| 62 | + if (!key || !value) { |
| 63 | + this.logger.warn('setCustomTags: key and value are required; ignoring call') |
| 64 | + return |
| 65 | + } |
| 66 | + |
| 67 | + const testInstance: TestFrameworkInstance = TestFramework.getTrackedInstance() |
| 68 | + if (!testInstance) { |
| 69 | + this.logger.warn('setCustomTags called outside of a resolvable test context; ignoring') |
| 70 | + return |
| 71 | + } |
| 72 | + |
| 73 | + const values = parseCommaSeparatedValues(value) |
| 74 | + if (values.length === 0) { |
| 75 | + this.logger.warn(`setCustomTags: no usable values parsed from "${value}"; ignoring call`) |
| 76 | + return |
| 77 | + } |
| 78 | + |
| 79 | + // Merge into the per-test instance map (test-scoped accumulator). |
| 80 | + const existing = (TestFramework.getState(testInstance, TestFrameworkConstants.KEY_CUSTOM_TAGS) as CustomMetadata) || {} |
| 81 | + mergeIntoTags(existing, key, values) |
| 82 | + testInstance.updateMultipleEntries({ |
| 83 | + [TestFrameworkConstants.KEY_CUSTOM_TAGS]: existing |
| 84 | + }) |
| 85 | + |
| 86 | + // Hook-level: if a hook is currently active, also stamp custom_metadata |
| 87 | + // onto the active hook so binary reads hook.custom_metadata. |
| 88 | + this.applyToActiveHook(testInstance, key, values) |
| 89 | + |
| 90 | + this.logger.debug(`setCustomTags: merged key=${key} values=${JSON.stringify(values)}`) |
| 91 | + } catch (error) { |
| 92 | + this.logger.warn(`setCustomTags: error while recording custom tags: ${error}`) |
| 93 | + } |
| 94 | + } |
| 95 | + } catch (error) { |
| 96 | + this.logger.error(`Error in CustomTagsModule.onBeforeExecute: ${error}`) |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * If the test framework is currently inside a hook, merge custom_metadata onto |
| 102 | + * the last-started hook record so the binary receives hook.custom_metadata. |
| 103 | + * Best-effort: silently no-ops when no active hook is resolvable. |
| 104 | + */ |
| 105 | + private applyToActiveHook(testInstance: TestFrameworkInstance, key: string, values: string[]) { |
| 106 | + try { |
| 107 | + const currentState = testInstance.getCurrentTestState().toString() |
| 108 | + if (!CLIUtils.matchHookRegex(currentState)) { |
| 109 | + return |
| 110 | + } |
| 111 | + const hook = WdioMochaTestFramework.lastActiveHook(testInstance, WdioMochaTestFramework.KEY_HOOK_LAST_STARTED) |
| 112 | + if (!hook) { |
| 113 | + return |
| 114 | + } |
| 115 | + const hookTags = (hook[TestFrameworkConstants.KEY_CUSTOM_TAGS] as CustomMetadata) || {} |
| 116 | + mergeIntoTags(hookTags, key, values) |
| 117 | + hook[TestFrameworkConstants.KEY_CUSTOM_TAGS] = hookTags |
| 118 | + } catch (error) { |
| 119 | + this.logger.debug(`setCustomTags: could not apply to active hook: ${error}`) |
| 120 | + } |
| 121 | + } |
| 122 | +} |
0 commit comments