How to Handle Multi-Select in Selenium and Playwright
Practice multi-select automation — native select, custom checkbox dropdowns, tag/pill removal, searchable multi-selects, and grouped options in Playwright, Selenium & Cypress.
Interactive Scenarios
Click to select a single option.
Hold Ctrl / Cmd to select multiple options.
Remove button has no data-testid. Target via parent [data-tag-value] → child button.
- JavaScript
- TypeScript
- Python
- Java
Options are grouped with <optgroup>. No data-testid on the groups — target via XPath: //optgroup[@label='Backend']//option[@value='node']
| ID | Scenario | Type | Priority | |
|---|---|---|---|---|
| MS_001 | Select a single option from a native multi-select Expected: Only the selected option is highlighted; selectedOptions.length === 1. ✅ positivehigh | positive | high | |
| MS_002 | Select multiple options simultaneously from native multi-select Expected: Both selected options are highlighted; selectedOptions.length === 2. ✅ positivehigh | positive | high | |
| MS_003 | Select all available options in native multi-select Expected: All options are selected; selectedOptions.length equals total option count. ✅ positivemedium | positive | medium | |
| MS_004 | Deselect a specific option while keeping others selected Expected: Deselected option is no longer highlighted; others remain selected. ✅ positivehigh | positive | high | |
| MS_005 | Native multi-select ignores deselection when no option is pre-selected Expected: No change in state; no error thrown. ⚠️ edgelow | edge | low | |
| MS_006 | Open custom checkbox multi-select and select an option Expected: Option is checked; selected count label updates. ✅ positivehigh | positive | high | |
| MS_007 | Close custom dropdown by clicking outside Expected: Panel is hidden after clicking outside the trigger area. ✅ positivemedium | positive | medium | |
| MS_008 | Use Select All button to check all custom options Expected: All options gain aria-selected=true; count label shows full count. ✅ positivehigh | positive | high | |
| MS_009 | Use Clear All button to deselect every custom option Expected: All options lose aria-selected; count label resets to 0. ✅ positivehigh | positive | high | |
| MS_010 | Remove a tag by clicking its close button (no data-testid on button) Expected: The removed tag no longer appears in the tag list. ✅ positivehigh | positive | high | |
| MS_011 | Filter options in searchable multi-select by typing Expected: Only matching options appear in the results listbox. ✅ positivehigh | positive | high | |
| MS_012 | Select a filtered option from searchable multi-select Expected: Selected option is added to the chosen list. ✅ positivehigh | positive | high | |
| MS_013 | Empty search returns all options in searchable multi-select Expected: All options appear in the results listbox when search is cleared. ⚠️ edgemedium | edge | medium | |
| MS_014 | Select an option from a grouped optgroup in native multi-select Expected: Option from the specified group is selected. ✅ positivemedium | positive | medium |
Multi-select controls come in two flavours: native HTML <select multiple> and custom JavaScript widgets built from divs, checkboxes, or comboboxes. Each requires a different automation approach. Playwright's selectOption handles native selects natively; custom widgets need role-based or attribute-based locators.
Playwright's selectOption accepts a single value, an array of values, or option labels. Selenium uses the Select helper class which wraps the <select> element and exposes selectByValue, selectByVisibleText, and deselectByValue. Cypress uses .select() which also accepts arrays.
// Select a single option by value
await page.getByTestId('ms-native-select').selectOption('playwright');
// Select multiple options by value array
await page.getByTestId('ms-native-select').selectOption(['playwright', 'cypress']);
// Select by visible label
await page.getByTestId('ms-native-select').selectOption({ label: 'Playwright' });
// Assert selected values
const selected = await page.getByTestId('ms-native-select').inputValue();
// For multi-select use evaluate:
const values = await page.getByTestId('ms-native-select').evaluate(
(el: HTMLSelectElement) => [...el.selectedOptions].map(o => o.value)
);
expect(values).toEqual(['playwright', 'cypress']);Custom dropdowns built from divs or checkboxes have no native selectOption support. You must click the trigger to open the panel, then click each option individually. Scope your locator to the panel's data-testid or role before clicking options to avoid targeting hidden duplicates.
// Open the custom dropdown
await page.getByTestId('ms-custom-trigger').click();
// Assert panel is visible
const panel = page.getByTestId('ms-custom-panel');
await expect(panel).toBeVisible();
// Click an option by its data-value attribute
await page.locator('[data-testid="ms-custom-option"][data-value="react"]').click();
// Scoped locator with filter (medium difficulty)
await panel.getByRole('option', { name: 'Vue.js' }).click();
// XPath: ancestor-scoped option click
// //div[@data-testid="ms-custom-panel"]//*[@role="option" and @data-value="angular"]
// Select All
await page.getByTestId('ms-select-all-btn').click();Searchable multi-selects expose a combobox input that filters a listbox. Fill the input to filter, then click the matching option. Use role='combobox' and role='option' for locating. When options have a stable data attribute (e.g. data-option-id), prefer that for robustness.
// Fill the combobox to filter options
await page.getByTestId('ms-search-input').fill('vue');
// Assert filtered results
const results = page.getByTestId('ms-search-results');
await expect(results).toBeVisible();
await expect(results.getByRole('option')).toHaveCount(1);
// Click matching option by data-option-id (stable attribute)
await page.locator('[role="option"][data-option-id="opt-vue"]').click();
// XPath: inside listbox by option id
// //*[@data-testid="ms-search-results"]//*[@role="option" and @data-option-id="opt-vue"]
// Assert chosen
await expect(page.getByTestId('ms-search-chosen')).toContainText('Vue.js');| Action | Selenium | Playwright JS | Playwright PY | Cypress |
|---|---|---|---|---|
| Select by value | Select.selectByValue('v') | selectOption('v') | select_option('v') | .select('v') |
| Select multiple | selectByValue() × N (Select class) | selectOption(['v1','v2']) | select_option(['v1','v2']) | .select(['v1','v2']) |
| Deselect by value | Select.deselectByValue('v') | selectOption(remaining[]) | select_option(remaining[]) | .select(remaining[]) |
| Custom option click | panel.findElement(By.css('[data-value]')).click() | locator('[data-value=\'v\']').click() | locator('[data-value="v"]').click() | .get('[data-value="v"]').click() |
| Child button (no testid) | tag.findElement(By.tagName('button')) | locator('[data-tag-value]').getByRole('button') | locator('[data-tag-value]').get_by_role('button') | .find('button').click() |