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
116 changes: 116 additions & 0 deletions ui-scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# UI Scripts for BrowserStack Test Management

A UI Script is JavaScript that an admin attaches to one or more surfaces in Test Management, such as the Create Test Case form or the result dialog. Every project member who opens that surface runs the script in their own browser, so one rule reaches everyone on the project.

This folder is the UI Script library. Each script is one `.js` file, in a folder named for the surface it runs on. The [UI Scripts documentation](https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts) explains the concepts, the [UI Script examples](https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples) page describes what each surface hands your callback, and the [TM toolkit reference](https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/tm-toolkit-reference) describes every `TM` function the scripts use.

> **Warning:** Treat every script in this folder as a starting point. Test it in a sandbox project and confirm the behavior before you enable it on a production project.

## Who can add a UI Script

Only an admin can configure UI Scripts. You need an IAM role of **Owner** or **Admin**, or the **Product Admin** role in Test Management. Every other role can work in a project that already has UI Scripts, but cannot open the **UI Scripts** page or edit a script. For the full role breakdown, see [Role-Based Access Control](https://www.browserstack.com/docs/test-management/user-access-control).

## How to use a script

1. Open the `.js` file for the recipe you want. The comment at the top names the recipe, the surface to select, and what the script does.
2. Replace any placeholder token, such as `<YOUR_PROJECT_IDS>` or `<YOUR_TRACKER_BASE_URL>`, with your own value.
3. Swap the sample field names for the names your own project uses. Most scripts use names such as **Target Region** and **Automation Link**. The run, plan, and session scripts that came from an engineering test project use names such as **UIS String** and **UIS Boolean**.
4. In Test Management, create a new UI Script, paste the code, select the surface named in the header comment, and save.
5. Reload the app before you test. The script engine loads every active script once, when Test Management loads, so a script you create, edit, enable, disable, or delete takes effect on the next page load.

## Three rules to know before you enable a script

- **Return the block.** A save callback blocks the save only when it returns the result of `TM.blockSave`. A call without `return` does nothing and the save goes through.
- **Enabled scripts run together.** Every enabled script on a surface runs, in priority order, and stops at the first script that blocks a save. To test one script on its own, disable the others on that surface. The scripts named "Combine rules" already bundle several behaviors, so do not enable them alongside the single-rule scripts they contain.
- **Trigger on text or boolean fields.** On the run, plan, and session forms, a dropdown hands `TM.onFieldChange` its stored value rather than the visible label. A text or boolean field hands you exactly what the tester entered, so build conditional rules on those.

## Folder map

Each folder holds the scripts for one surface.

| Folder | Surface | Scripts |
| ------ | ------- | ------- |
| `add-result/` | Add Result and Update Result | 2 |
| `test-case-results-toolbar/` | Test Case Results Toolbar | 3 |
| `test-case-form/` | Test Case Form | 18 |
| `test-run-form/` | Test Run Form | 9 |
| `test-plan-form/` | Test Plan Form | 9 |
| `exploratory-session-form/` | Exploratory Session Form | 9 |

The scripts in `add-result/` register the `add_result` surface in code. Select both the Add Result and Update Result surfaces when you save the script, so the rule applies whether a tester logs a new result or edits an existing one.

### Add Result and Update Result

- `require-notes-on-a-failed-result.js`
- `require-a-linked-defect-on-a-failed-result.js`

### Test Case Results Toolbar

- `file-a-pre-filled-bug-in-your-tracker.js`
- `open-a-runbook-from-the-toolbar.js`
- `run-your-own-handler-from-a-dialog-button.js`

### Test Case Form

Validation:

- `enforce-a-title-naming-convention.js`
- `require-at-least-one-non-empty-step.js`
- `require-preconditions.js`
- `require-a-linked-requirement.js`
- `require-one-custom-field-based-on-another.js`
- `require-a-tag-on-critical-test-cases.js`
- `forbid-an-invalid-automation-status-and-type-combination.js`

Field behavior:

- `lock-a-field-against-editing.js`
- `hide-a-field.js`
- `pre-fill-a-custom-dropdown.js`
- `restrict-the-options-on-a-dropdown.js`
- `show-a-field-only-when-priority-is-critical.js`
- `require-a-field-when-automation-status-becomes-automated.js`
- `hide-or-lock-a-built-in-field.js`
- `make-an-optional-field-required.js`

Buttons and dialogs:

- `add-a-form-button-that-opens-a-dialog.js`
- `add-a-form-button-that-opens-a-link.js`
- `build-a-dialog-with-your-own-buttons.js`

### Test Run Form

- `enforce-a-run-name-convention.js`
- `require-a-test-plan-on-every-run.js`
- `require-a-test-plan-when-the-title-says-automatable.js`
- `require-a-field-when-the-title-marks-a-run-urgent.js`
- `show-or-hide-fields-on-the-run-form.js`
- `lock-assign-run-and-configurations.js`
- `pre-fill-defaults-when-a-form-opens.js`
- `set-several-field-behaviors-when-the-run-form-opens.js`
- `combine-rules-on-the-run-form.js`

### Test Plan Form

- `enforce-a-test-plan-name-convention.js`
- `require-a-linked-requirement-on-a-test-plan.js`
- `require-a-dropdown-when-a-text-field-says-automatable.js`
- `require-a-dropdown-when-a-checkbox-is-on.js`
- `show-or-hide-fields-on-the-test-plan-form.js`
- `lock-a-field-on-the-test-plan-form.js`
- `pre-fill-defaults-on-the-test-plan-form.js`
- `keep-the-end-date-after-the-start-date.js`
- `combine-rules-on-the-test-plan-form.js`

### Exploratory Session Form

- `enforce-a-session-name-convention.js`
- `require-a-linked-requirement-on-a-session.js`
- `require-a-dropdown-when-a-text-field-says-automatable.js`
- `require-a-dropdown-when-a-checkbox-is-on.js`
- `show-or-hide-fields-on-the-session-form.js`
- `lock-a-field-on-the-session-form.js`
- `pre-fill-defaults-on-the-session-form.js`
- `limit-the-session-timebox.js`
- `combine-rules-on-the-session-form.js`
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Recipe: Require a linked defect on a Failed result
// Surface: Add Result and Update Result (select both)
// A Failed result should point at the defect it produced.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onBeforeSave('add_result', function (formState) {
if (formState.status.label === 'Failed' &&
(!formState.defects || formState.defects.length === 0)) {
return TM.blockSave('Link at least one defect when the result is Failed.');
}
});
22 changes: 22 additions & 0 deletions ui-scripts/add-result/require-notes-on-a-failed-result.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Recipe: Require Notes on a Failed result
// Surface: Add Result and Update Result (select both)
// A tester should not be able to log a Failed result without explaining the failure.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onBeforeSave('add_result', function (formState) {
var isEmptyRichText = function (html) {
if (!html) return true;

// Let the browser parse the markup and read the text back, instead of stripping the
// tags with a regular expression. A regex leaves an unterminated tag such as
// '<script src=x' untouched, and parsing decodes entities such as &nbsp; for you.
// parseFromString only builds a document, so it never runs the markup it is given.
var parsed = new DOMParser().parseFromString(String(html), 'text/html');
var text = (parsed.body.textContent || '').replace(/\s+/g, '');
return text.length === 0;
};

if (formState.status.label === 'Failed' && isEmptyRichText(formState.notes)) {
return TM.blockSave('Notes are required when the result is Failed.');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Recipe: Combine rules on the session form
// Surface: Exploratory Session Form
// Require Timebox, pre-fill the title, shape custom fields, drive Test Plan from URGENT in the title, and enforce the [ES] prefix in one script. Custom field names such as UIS String and UIS Boolean come from a sample project. Swap in the names your own project uses.

// onFormLoad - fires once when the form opens
TM.onFormLoad('exploratory_session_form', function (form) {
TM.setFieldRequired('timebox', true); // built-in Timebox shows the required *
TM.setFieldValue('title', '[ES] '); // prefill the session title
TM.setFieldRequired('UIS String', true); // a custom field, by display name
TM.setFieldVisible('UIS Url', false); // hide a custom field
TM.setFieldReadOnly('UIS Integer', true); // custom field input disabled
});

// onFieldChange - conditional / bidirectional (a text or boolean trigger is reliable)
// Toggle "UIS Boolean" ON -> Dropdown required, MultiSel hidden. OFF -> revert.
TM.onFieldChange('UIS Boolean', function (val) {
var on = (val === true || val === 'true');
TM.setFieldRequired('UIS Dropdown', on);
TM.setFieldVisible('UIS MultiSel', !on);
});

// onFieldChange on a standard field (title) driving a standard field
// Type "URGENT" anywhere in the Title -> Test Plan becomes required.
TM.onFieldChange('title', function (val) {
var v = (val && val.value !== undefined) ? val.value : (val || '');
TM.setFieldRequired('testPlan', String(v).indexOf('URGENT') >= 0);
});

// onBeforeSave - block with a message (note the RETURN)
TM.onBeforeSave('exploratory_session_form', function (form) {
if (!/^\[ES\]/.test(form.title || '')) {
return TM.blockSave('Title must start with [ES]', 'title');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Recipe: Enforce a session name convention
// Surface: Exploratory Session Form
// Block the save unless the session title starts with [ES]. The error renders inline under the title.

TM.onBeforeSave('exploratory_session_form', function (form) {
if (!/^\[ES\]/.test(form.title || '')) {
return TM.blockSave('Title must start with [ES]', 'title');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Recipe: Limit the session timebox
// Surface: Exploratory Session Form
// This rule keeps the session timebox between 1 and 480 minutes.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onBeforeSave('exploratory_session_form', function (form) {
var minutes = Number(form.timebox);
if (!form.timebox || isNaN(minutes) || minutes < 1) {
return TM.blockSave('Timebox must be at least 1 minute.', 'timebox');
}
if (minutes > 480) {
return TM.blockSave('Timebox cannot exceed 480 minutes.', 'timebox');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Recipe: Lock a field on the session form
// Surface: Exploratory Session Form
// Disable a custom field when the form opens. Custom field names such as UIS String and UIS Boolean come from a sample project. Swap in the names your own project uses.

TM.onFormLoad('exploratory_session_form', function () { TM.setFieldReadOnly('UIS Integer', true); });
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Recipe: Pre-fill defaults on the session form
// Surface: Exploratory Session Form
// Pre-fill the title, a 60 minute timebox, the charter in Description, and a custom field when the form opens. Custom field names such as UIS String and UIS Boolean come from a sample project. Swap in the names your own project uses.

TM.onFormLoad('exploratory_session_form', function () {
TM.setFieldValue('title', '[ES] Ad-hoc exploratory session'); // standard field
TM.setFieldValue('timebox', '60'); // 60 minutes
TM.setFieldValue('description', 'Charter set by UI Script'); // standard RTE field
TM.setFieldValue('UIS String', 'Auto-filled by policy'); // custom field
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Recipe: Require a dropdown when a checkbox is on
// Surface: Exploratory Session Form
// Toggle the boolean field on and the dropdown becomes required. Toggle it off and the requirement clears. Custom field names such as UIS String and UIS Boolean come from a sample project. Swap in the names your own project uses.

TM.onFieldChange('UIS Boolean', function (val) {
TM.setFieldRequired('UIS Dropdown', (val === true || val === 'true'));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Recipe: Require a dropdown when a text field says Automatable
// Surface: Exploratory Session Form
// A text field is a reliable trigger because its value is exactly what the tester types. Custom field names such as UIS String and UIS Boolean come from a sample project. Swap in the names your own project uses.

TM.onFieldChange('UIS String', function (val) {
var v = (val && val.value !== undefined) ? val.value : (val || '');
TM.setFieldRequired('UIS Dropdown', String(v).toLowerCase().indexOf('automatable') >= 0);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Recipe: Require a linked requirement on a session
// Surface: Exploratory Session Form
// Block the save until at least one requirement is linked. The same shape works with testPlan to require a linked test plan.

TM.onBeforeSave('exploratory_session_form', function (form) {
if (!form.requirements || form.requirements.length === 0) {
return TM.blockSave('Link at least one requirement before saving', 'requirements'); // inline error on the Requirements field
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Recipe: Show or hide fields on the session form
// Surface: Exploratory Session Form
// Hide one custom field when the form opens, and hide another while a boolean field is on. Custom field names such as UIS String and UIS Boolean come from a sample project. Swap in the names your own project uses.

// Static hide on load:
TM.onFormLoad('exploratory_session_form', function () { TM.setFieldVisible('UIS Url', false); });

// Conditional hide (UIS MultiSel hidden while UIS Boolean is ON):
TM.onFieldChange('UIS Boolean', function (val) {
TM.setFieldVisible('UIS MultiSel', !(val === true || val === 'true'));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Recipe: Add a form button that opens a dialog
// Surface: Test Case Form
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.addButton('test_case_form', { label: 'Helper' });

TM.onButtonClick('Helper', function () {
TM.openDialog({
title: 'Helper',
content: TM.ui.Stack({ gap: 4 }, [
TM.ui.Text({}, 'Hello from a UI Script'),
TM.ui.Row({ justify: 'end' }, [
TM.ui.Button({ variant: 'primary', action: 'cancel' }, 'OK')
])
])
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Recipe: Add a form button that opens a link
// Surface: Test Case Form
// TM.openUrl opens http and https addresses only and ignores every other scheme.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.addButton('test_case_form', { label: 'Open Docs' });

TM.onButtonClick('Open Docs', function () {
TM.openUrl('https://www.browserstack.com/docs/test-management');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Recipe: Build a dialog with your own buttons
// Surface: Test Case Form
// A dialog built from a content tree renders no footer, so you supply the buttons.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.addButton('test_case_form', { label: 'Open Dialog' });

TM.onButtonClick('Open Dialog', function () {
TM.openDialog({
title: 'Dialog',
content: TM.ui.Stack({ gap: 8 }, [
TM.ui.Text({}, 'Pick an action'),
TM.ui.TextField({ name: 'note', label: 'Note' }),
TM.ui.Button({ action: 'stayOpen' }, 'Stays open'),
TM.ui.Button({ variant: 'primary', action: 'done' }, 'Run and close')
])
});
});

TM.onAction('done', function (data) {
// Do your work with data.note, then close.
TM.closeDialog();
});
10 changes: 10 additions & 0 deletions ui-scripts/test-case-form/enforce-a-title-naming-convention.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Recipe: Enforce a title naming convention
// Surface: Test Case Form
// The error renders under the Title field. Change the regular expression to match your own convention.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onBeforeSave('test_case_form', function (form) {
if (!/^TC-/.test(form.title || '')) {
return TM.blockSave("Title must start with 'TC-'.", 'Title');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Recipe: Forbid an invalid Automation Status and Type combination
// Surface: Test Case Form
// Both fields are system fields, so both comparisons use internal_name.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onBeforeSave('test_case_form', function (form) {
var automation = form.automationStatus && form.automationStatus.internal_name;
var caseType = form.caseType && form.caseType.internal_name;
if (automation === 'automated' && caseType === 'other') {
return TM.blockSave('An automated test case cannot have the type Other.');
}
});
8 changes: 8 additions & 0 deletions ui-scripts/test-case-form/hide-a-field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Recipe: Hide a field
// Surface: Test Case Form
// TM.setFieldVisible(name, false) removes the field from the form entirely.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onFormLoad('test_case_form', function () {
TM.setFieldVisible('Target Region', false);
});
10 changes: 10 additions & 0 deletions ui-scripts/test-case-form/hide-or-lock-a-built-in-field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Recipe: Hide or lock a built-in field
// Surface: Test Case Form
// Built-in fields are addressed by their form label.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onFormLoad('test_case_form', function () {
TM.setFieldReadOnly('Tags', true);
TM.setFieldVisible('Preconditions', false);
TM.setFieldVisible('Steps', false);
});
7 changes: 7 additions & 0 deletions ui-scripts/test-case-form/lock-a-field-against-editing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Recipe: Lock a field against editing
// Surface: Test Case Form
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onFormLoad('test_case_form', function () {
TM.setFieldReadOnly('Target Region', true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Recipe: Make an optional field required
// Surface: Test Case Form
// The field gains a required marker and blocks the save while it is empty.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onFormLoad('test_case_form', function () {
TM.setFieldRequired('Target Region', true);
});
8 changes: 8 additions & 0 deletions ui-scripts/test-case-form/pre-fill-a-custom-dropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Recipe: Pre-fill a custom dropdown
// Surface: Test Case Form
// On a custom dropdown, TM.setFieldValue takes an option object shaped as { value, label }.
// Docs: https://www.browserstack.com/docs/test-management/advanced-features/ui-scripts/ui-script-examples

TM.onFormLoad('test_case_form', function () {
TM.setFieldValue('Target Region', { value: 'EMEA', label: 'EMEA' });
});
Loading