Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/pr-193.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@wdio/browserstack-service": patch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a new public API (browser.uploadAttachment / browser.uploadMedia), so per the PR template it should be a minor bump, not patch ("New features mislabeled as patch is the common mistake — when unsure, choose minor"). The release-notes type should also be New Feature rather than Bug Fix.

Fix: tick minor + New Feature in the Release section; the changeset regenerates from it.

---

- Added `browser.uploadAttachment(filePath)` (also available as `browser.uploadMedia`) so WebdriverIO tests can attach files to a test, hook, or build in Test Reporting — the same capability the Java, Python and Node SDKs already offer. Pass `{ buildAttachment: true }` to attach to the build instead of the current test.
- Made BrowserStack session bootstrap tolerant of an incomplete configuration response. Previously an empty or partial response aborted the whole bootstrap, which silently disabled every BrowserStack feature for that run — including custom tags and Test Reporting — and could leave the build with no test results.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
declare namespace WebdriverIO {
interface Browser {
setCustomTags: (key: string, value: string) => Promise<void>
uploadAttachment: (filePath: string, options?: { buildAttachment?: boolean }) => Promise<void>
uploadMedia: (filePath: string, options?: { buildAttachment?: boolean }) => Promise<void>
}

interface MultiRemoteBrowser {
setCustomTags: (key: string, value: string) => Promise<void>
uploadAttachment: (filePath: string, options?: { buildAttachment?: boolean }) => Promise<void>
uploadMedia: (filePath: string, options?: { buildAttachment?: boolean }) => Promise<void>
}
}
62 changes: 51 additions & 11 deletions packages/browserstack-service/src/cli/apiUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BStackLogger } from './cliLogger.js'

export default class APIUtils {
static FUNNEL_INSTRUMENTATION_URL = 'https://api.browserstack.com/sdk/v1/event'
static BROWSERSTACK_AUTOMATE_API_URL = 'https://api.browserstack.com'
Expand All @@ -10,16 +12,54 @@ export default class APIUtils {
static UPLOAD_LOGS_ADDRESS = 'https://upload-observability.browserstack.com'
static EDS_URL = 'https://eds.browserstack.com'

static updateURLSForGRR(apis: GRRUrls) {
this.FUNNEL_INSTRUMENTATION_URL = `${apis.automate.api}/sdk/v1/event`
this.BROWSERSTACK_AUTOMATE_API_URL = apis.automate.api
this.BROWSERSTACK_AA_API_URL = apis.appAutomate.api
this.BROWSERSTACK_PERCY_API_URL = apis.percy.api
this.BROWSERSTACK_AUTOMATE_API_CLOUD_URL = apis.automate.upload
this.BROWSERSTACK_AA_API_CLOUD_URL = apis.appAutomate.upload
this.APP_ALLY_ENDPOINT = `${apis.appAccessibility.api}/automate`
this.DATA_ENDPOINT = apis.observability.api
this.UPLOAD_LOGS_ADDRESS = apis.observability.upload
this.EDS_URL = apis.edsInstrumentation.api
/**
* Overlay the binary-supplied GRR endpoints onto the public defaults. Every field is
* optional: a degenerate StartBinSession/ConnectBinSession config (auth failure, empty
* payload) used to throw here and abort the whole CLI bootstrap, taking every product
* module with it. Missing entries now just leave the corresponding default in place.
*/
static updateURLSForGRR(apis?: GRRUrls) {
if (!apis) {
BStackLogger.debug('updateURLSForGRR: no apis in the bin-session config; keeping default endpoints')
return
}
const missing = [
['automate.api', apis.automate?.api], ['automate.upload', apis.automate?.upload],
['appAutomate.api', apis.appAutomate?.api], ['appAutomate.upload', apis.appAutomate?.upload],
['percy.api', apis.percy?.api], ['appAccessibility.api', apis.appAccessibility?.api],
['observability.api', apis.observability?.api], ['observability.upload', apis.observability?.upload],
['edsInstrumentation.api', apis.edsInstrumentation?.api]
].filter(([, value]) => !value).map(([name]) => name)
if (missing.length) {
BStackLogger.debug(`updateURLSForGRR: keeping default endpoints for ${missing.join(', ')}`)
}
if (apis.automate?.api) {
this.FUNNEL_INSTRUMENTATION_URL = `${apis.automate.api}/sdk/v1/event`
this.BROWSERSTACK_AUTOMATE_API_URL = apis.automate.api
}
if (apis.automate?.upload) {
this.BROWSERSTACK_AUTOMATE_API_CLOUD_URL = apis.automate.upload
}
if (apis.appAutomate?.api) {
this.BROWSERSTACK_AA_API_URL = apis.appAutomate.api
}
if (apis.appAutomate?.upload) {
this.BROWSERSTACK_AA_API_CLOUD_URL = apis.appAutomate.upload
}
if (apis.percy?.api) {
this.BROWSERSTACK_PERCY_API_URL = apis.percy.api
}
if (apis.appAccessibility?.api) {
this.APP_ALLY_ENDPOINT = `${apis.appAccessibility.api}/automate`
}
if (apis.observability?.api) {
this.DATA_ENDPOINT = apis.observability.api
}
if (apis.observability?.upload) {
this.UPLOAD_LOGS_ADDRESS = apis.observability.upload
}
if (apis.edsInstrumentation?.api) {
this.EDS_URL = apis.edsInstrumentation.api
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ export const TestFrameworkConstants = {
DEFAULT_HOOK_RESULT : 'pending',
KIND_SCREENSHOT : 'TEST_SCREENSHOT',
KIND_LOG : 'TEST_LOG',
KIND_ATTACHMENT : 'TEST_ATTACHMENT',
HOOK_REGEX : '^(BEFORE_|AFTER_)',
}
13 changes: 13 additions & 0 deletions packages/browserstack-service/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import WdioAutomationFramework from './frameworks/wdioAutomationFramework.js'
import WebdriverIOModule from './modules/webdriverIOModule.js'
import AccessibilityModule from './modules/accessibilityModule.js'
import CustomTagsModule from './modules/customTagsModule.js'
import UploadAttachmentModule from './modules/uploadAttachmentModule.js'
import { isTurboScale, processAccessibilityResponse, shouldAddServiceVersion } from '../util.js'
import ObservabilityModule from './modules/observabilityModule.js'
import type { BrowserstackConfig, BrowserstackOptions, LaunchResponse } from '../types.js'
Expand Down Expand Up @@ -184,6 +185,10 @@ export class BrowserstackCLI {
// to TestHub, so it is gated on the testhub pipeline being active.
this.modules[CustomTagsModule.MODULE_NAME] = new CustomTagsModule()

// Attachments ride a TEST_ATTACHMENT LogCreated event keyed on the test /
// hook uuid, so they are gated on the same pipeline.
this.modules[UploadAttachmentModule.MODULE_NAME] = new UploadAttachmentModule()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uploadAttachment / uploadMedia are only registered inside if (startBinResponse.testhub), and never on the classic (non-CLI) path (launcher.ts classicBuildStartAttempted). So whenever Test Reporting is off, build start fails, or the CLI isn't running, browser.uploadMedia(...) still throws TypeError: driver.uploadMedia is not a function — the exact symptom SDK-7138 reports — while the .d.ts declares the method unconditionally, so TS users get no warning.

The sibling SDKs never throw here: Python's upload_attachment and Java's BrowserStack.uploadAttachment are always callable and just no-op/log when reporting is inactive.

Fix: always attach a no-op stub (debug log) to the browser, and let UploadAttachmentModule overwrite it when testhub is up.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that was the SDK-7138 symptom surviving on every non-testhub path. Fixed in e4dc5cd: service.before() now calls UploadAttachmentModule.installNoopFallback(browser), which installs a no-op uploadAttachment/uploadMedia (debug log "Test Reporting is not active") whenever the browser doesn't already have one. That covers the classic path, no testhub, and CLI down. onBeforeExecute still replaces it with the real implementation when the binary is up, and an already-registered implementation is left alone. Unit-tested all three cases.


if (startBinResponse.accessibility?.success){
process.env[BROWSERSTACK_ACCESSIBILITY] = 'true'
const options = this.options as BrowserstackConfig & BrowserstackOptions
Expand Down Expand Up @@ -529,6 +534,14 @@ export class BrowserstackCLI {
*/
setConfig(response: StartBinSessionResponse) {
try {
// A degenerate bin-session response (auth failure, races on a parallel worker's
// ConnectBinSession) carries an empty config. JSON.parse would throw, leaving
// this.config on its previous value and the error indistinguishable from a
// malformed payload — keep the empty default and say so.
if (!response.config || !response.config.trim()) {
this.logger.warn('setConfig: bin session returned an empty config; continuing with defaults')
return
}
this.config = JSON.parse(response.config)
// Binary now nests apis under config.sessionData; prefer it, fall back to the flat config.apis (SDK-6821 Phase 3)
const sessionData = this.config.sessionData as { apis?: unknown } | undefined
Expand Down
Loading
Loading