From 6aab2ecb5c4c5be0e0e1a26e78d676a641b59516 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Tue, 22 Sep 2026 20:30:15 +0530 Subject: [PATCH 1/3] fix(observability): report Mocha/Jasmine test tags WDIO Mocha and Jasmine never populated a test-level tags field, so every Observability test arrived with tags == []. Neither runner has a tag construct, so @tag tokens in the suite and test titles are the source -- the convention the node SDK already uses for Jest and Playwright. The leading @ is kept so these match the Cucumber runner's pickle tags. Both event paths were missing it: - Binary Flow: wdioMochaTestFramework#getTestData omitted KEY_TEST_TAGS. That 'test_tags' key was declared in the service's own constants and never written anywhere, while the binary has long consumed it (observability/languages/webdriverio/test-frameworks/mocha: tags = event.test_tags || []). Fixing only the Direct Flow would have left the CLI path -- the one App Automate runs -- still emitting []. - Direct Flow: insights-handler#getRunData built TestData with no tags key; tags were set only in the Cucumber-specific path. Inert for untagged suites: a title with no @ token yields [], so existing payloads are unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../cli/frameworks/wdioMochaTestFramework.ts | 6 ++-- .../src/insights-handler.ts | 6 +++- packages/browserstack-service/src/util.ts | 21 ++++++++++++ .../browserstack-service/tests/util.test.ts | 34 +++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/browserstack-service/src/cli/frameworks/wdioMochaTestFramework.ts b/packages/browserstack-service/src/cli/frameworks/wdioMochaTestFramework.ts index 3c7df71c..8a885982 100644 --- a/packages/browserstack-service/src/cli/frameworks/wdioMochaTestFramework.ts +++ b/packages/browserstack-service/src/cli/frameworks/wdioMochaTestFramework.ts @@ -10,7 +10,7 @@ import TrackedInstance from '../instances/trackedInstance.js' import { TestFrameworkConstants } from './constants/testFrameworkConstants.js' import { BStackLogger as logger } from '../cliLogger.js' import type { Frameworks } from '@wdio/types' -import { getMochaTestHierarchy, getUniqueIdentifier, isUndefined, removeAnsiColors } from '../../util.js' +import { getMochaTestHierarchy, getTestTags, getUniqueIdentifier, isUndefined, removeAnsiColors } from '../../util.js' import { TEST_ANALYTICS_ID } from '../../constants.js' /** @@ -216,6 +216,7 @@ export default class WdioMochaTestFramework extends TestFramework { const framework = TestFramework.getState(instance, TestFrameworkConstants.KEY_TEST_FRAMEWORK_NAME) const fullTitle = getUniqueIdentifier(test, framework) const filename = test.file // || this._suiteFile + const scopes = getMochaTestHierarchy(test) const testData: Record = { [TestFrameworkConstants.KEY_TEST_ID]: getUniqueIdentifier(test, framework), @@ -223,7 +224,8 @@ export default class WdioMochaTestFramework extends TestFramework { [TestFrameworkConstants.KEY_TEST_CODE]: test.body || '', ...resolveTestFilePaths(filename), [TestFrameworkConstants.KEY_TEST_SCOPE]: fullTitle, - [TestFrameworkConstants.KEY_TEST_SCOPES]: getMochaTestHierarchy(test), + [TestFrameworkConstants.KEY_TEST_SCOPES]: scopes, + [TestFrameworkConstants.KEY_TEST_TAGS]: getTestTags(test, scopes), } return testData diff --git a/packages/browserstack-service/src/insights-handler.ts b/packages/browserstack-service/src/insights-handler.ts index a38363e1..7a85d72a 100644 --- a/packages/browserstack-service/src/insights-handler.ts +++ b/packages/browserstack-service/src/insights-handler.ts @@ -16,6 +16,7 @@ import { getHookType, getPlatformVersion, getResolvedDeviceName, getScenarioExamples, + getTestTags, getUniqueIdentifier, getUniqueIdentifierForCucumber, isBrowserstackSession, @@ -942,6 +943,8 @@ class _InsightsHandler { InsightsHandler.currentTest.name = test.title || test.description } + const scopes = this.getHierarchy(test) + const testData: TestData = { uuid: testMetaData.uuid, type: test.type || 'test', @@ -951,7 +954,8 @@ class _InsightsHandler { code: test.body }, scope: fullTitle, - scopes: this.getHierarchy(test), + scopes, + tags: getTestTags(test, scopes), identifier: fullTitle, file_name: filename ? path.relative(process.cwd(), filename) : undefined, location: filename ? path.relative(process.cwd(), filename) : undefined, diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index c78ac37c..a1dd4091 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -2294,6 +2294,27 @@ export function getMochaTestHierarchy(test: Frameworks.Test) { return value.reverse() } +const TEST_TAG_PATTERN = /@[\w-]+/g + +/** + * Mocha and Jasmine have no tag construct, so `@tag` tokens written into the suite and + * test titles are the tag source — the same convention the node SDK uses for Jest and + * Playwright. The leading `@` is kept so these match the Cucumber runner's pickle tags, + * which reach Observability with it intact. + */ +export function getTestTags(test: Frameworks.Test, scopes?: string[]): string[] { + const titles = [...(scopes ?? getMochaTestHierarchy(test)), test.title || test.description || ''] + const tags: string[] = [] + for (const title of titles) { + for (const tag of title.match(TEST_TAG_PATTERN) || []) { + if (!tags.includes(tag)) { + tags.push(tag) + } + } + } + return tags +} + /** * True only for the hub-interpreted `browserstack_executor: {…}` magic string. * Anchored to the start (leading whitespace tolerated) and case-sensitive, matching diff --git a/packages/browserstack-service/tests/util.test.ts b/packages/browserstack-service/tests/util.test.ts index 64b02c05..5a0cce98 100644 --- a/packages/browserstack-service/tests/util.test.ts +++ b/packages/browserstack-service/tests/util.test.ts @@ -2430,3 +2430,37 @@ describe('coerceStringBooleans (SDK-3737)', () => { expect(utils.coerceStringBooleans({})).toEqual({}) }) }) + +describe('getTestTags', () => { + const tagsFor = (title: string, scopes: string[] = []) => + utils.getTestTags({ title } as any, scopes) + + it('picks up a tag in the test title', () => { + expect(tagsFor('logs in @smoke')).toEqual(['@smoke']) + }) + + it('picks up a tag from the describe scope', () => { + expect(tagsFor('logs in', ['auth @regression'])).toEqual(['@regression']) + }) + + it('merges scope and title tags, deduped', () => { + expect(tagsFor('logs in @smoke', ['auth @smoke', 'nested @regression'])) + .toEqual(['@smoke', '@regression']) + }) + + it('returns an empty array when nothing is tagged', () => { + expect(tagsFor('logs in', ['auth'])).toEqual([]) + }) + + it('picks up multiple tags from one title', () => { + expect(tagsFor('logs in @smoke @p1')).toEqual(['@smoke', '@p1']) + }) + + it('keeps hyphens in a tag', () => { + expect(tagsFor('logs in @smoke-test')).toEqual(['@smoke-test']) + }) + + it('falls back to the Jasmine description when there is no title', () => { + expect(utils.getTestTags({ description: 'logs in @jasmine' } as any, [])).toEqual(['@jasmine']) + }) +}) From 4bc51b5f25e50e0bf68f0f8662d08edb705ad857 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:03:26 +0000 Subject: [PATCH 2/3] chore(changeset): auto-generate from PR template (patch) --- .changeset/pr-214.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pr-214.md diff --git a/.changeset/pr-214.md b/.changeset/pr-214.md new file mode 100644 index 00000000..b7406483 --- /dev/null +++ b/.changeset/pr-214.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": patch +--- + +- Fixed test tags not being reported to Test Observability for Mocha and Jasmine. Tags written as `@tag` tokens in suite or test titles are now sent with each test. From f8c49f8e52d4e9e361fb316943e8b10a8c7a4aba Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Wed, 23 Sep 2026 11:16:34 +0530 Subject: [PATCH 3/3] fix(observability): address review on Mocha/Jasmine test tags - Tag pattern now requires the @ to start a token. /@[\w-]+/g matched any embedded @, so 'user@example.com' in a title produced a bogus @example tag and 'pkg@1.2.3' produced @1. Both now yield []. - Jasmine really is covered now. insights-handler#getRunData is reached only for mocha -- beforeTest and afterTest both early-return otherwise -- so jasmine events, and mocha skipped tests, go through reporter.ts#getRunData, which set scopes but no tags. It now sets tags too. Without this the Jasmine claim in the release notes was false. - Dropped the node-agent parity claim from the doc comment. That SDK strips the @ for Jest/Playwright (pwUtils.js), so the two emit different shapes; the @ is kept here to match this service's own Cucumber pickle tags, and the comment now says that instead. - Tests: added the embedded-@ cases and one case per hierarchy shape with scopes omitted, which exercises the getMochaTestHierarchy branch the previous suite never reached. Both orderings pinned to what the helper actually returns. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/reporter.ts | 4 +++- packages/browserstack-service/src/util.ts | 10 ++++++---- .../browserstack-service/tests/util.test.ts | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/browserstack-service/src/reporter.ts b/packages/browserstack-service/src/reporter.ts index 1c48f50d..68d77738 100644 --- a/packages/browserstack-service/src/reporter.ts +++ b/packages/browserstack-service/src/reporter.ts @@ -22,7 +22,8 @@ import { getPlatformVersion, getResolvedDeviceName, isObjectEmpty, - generateHashCodeFromFields + generateHashCodeFromFields, + getTestTags } from './util.js' import { BStackLogger } from './bstackLogger.js' import type { Capabilities } from '@wdio/types' @@ -294,6 +295,7 @@ class _TestReporter extends WDIOReporter { }, scope: scope, scopes: scopes, + tags: getTestTags(testStats as unknown as Frameworks.Test, scopes), identifier: identifier, file_name: suiteFileName ? path.relative(process.cwd(), suiteFileName) : undefined, location: suiteFileName ? path.relative(process.cwd(), suiteFileName) : undefined, diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index a1dd4091..5f5e0282 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -2294,13 +2294,15 @@ export function getMochaTestHierarchy(test: Frameworks.Test) { return value.reverse() } -const TEST_TAG_PATTERN = /@[\w-]+/g +// The lookbehind is load-bearing: without it every `@` starts a match, so an address +// like `user@example.com` in a title yields a bogus `@example` tag. +const TEST_TAG_PATTERN = /(? { it('falls back to the Jasmine description when there is no title', () => { expect(utils.getTestTags({ description: 'logs in @jasmine' } as any, [])).toEqual(['@jasmine']) }) + + it('ignores an @ embedded in a larger token', () => { + expect(tagsFor('sends the invite to user@example.com')).toEqual([]) + expect(tagsFor('installs pkg@1.2.3')).toEqual([]) + }) + + it('derives scopes from the mocha hierarchy when none are supplied', () => { + const test = { + title: 'logs in @smoke', + ctx: { test: {} }, + parent: { title: 'auth @regression', parent: { title: '' } } + } + expect(utils.getTestTags(test as any)).toEqual(['@regression', '@smoke']) + }) + + it('derives scopes from the jasmine hierarchy when none are supplied', () => { + const test = { description: 'logs in @smoke', fullName: 'auth @regression logs in @smoke' } + expect(utils.getTestTags(test as any)).toEqual(['@regression', '@smoke']) + }) })