Skip to content
Merged
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
---

- 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()

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