|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.io/license |
| 7 | + */ |
| 8 | + |
| 9 | +import {coerceBooleanProperty} from '@angular/cdk/coercion'; |
| 10 | +import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; |
| 11 | +import {RadioButtonHarnessFilters, RadioGroupHarnessFilters} from './radio-harness-filters'; |
| 12 | + |
| 13 | +/** Harness for interacting with an MDC-based mat-radio-group in tests. */ |
| 14 | +export class MatRadioGroupHarness extends ComponentHarness { |
| 15 | + /** The selector for the host element of a `MatRadioGroup` instance. */ |
| 16 | + static hostSelector = '.mat-mdc-radio-group'; |
| 17 | + |
| 18 | + /** |
| 19 | + * Gets a `HarnessPredicate` that can be used to search for a `MatRadioGroupHarness` that meets |
| 20 | + * certain criteria. |
| 21 | + * @param options Options for filtering which radio group instances are considered a match. |
| 22 | + * @return a `HarnessPredicate` configured with the given options. |
| 23 | + */ |
| 24 | + static with(options: RadioGroupHarnessFilters = {}): HarnessPredicate<MatRadioGroupHarness> { |
| 25 | + return new HarnessPredicate(MatRadioGroupHarness, options) |
| 26 | + .addOption('name', options.name, this._checkRadioGroupName); |
| 27 | + } |
| 28 | + |
| 29 | + /** Gets the name of the radio-group. */ |
| 30 | + async getName(): Promise<string|null> { |
| 31 | + const hostName = await this._getGroupNameFromHost(); |
| 32 | + // It's not possible to always determine the "name" of a radio-group by reading |
| 33 | + // the attribute. This is because the radio-group does not set the "name" as an |
| 34 | + // element attribute if the "name" value is set through a binding. |
| 35 | + if (hostName !== null) { |
| 36 | + return hostName; |
| 37 | + } |
| 38 | + // In case we couldn't determine the "name" of a radio-group by reading the |
| 39 | + // "name" attribute, we try to determine the "name" of the group by going |
| 40 | + // through all radio buttons. |
| 41 | + const radioNames = await this._getNamesFromRadioButtons(); |
| 42 | + if (!radioNames.length) { |
| 43 | + return null; |
| 44 | + } |
| 45 | + if (!this._checkRadioNamesInGroupEqual(radioNames)) { |
| 46 | + throw Error('Radio buttons in radio-group have mismatching names.'); |
| 47 | + } |
| 48 | + return radioNames[0]!; |
| 49 | + } |
| 50 | + |
| 51 | + /** Gets the id of the radio-group. */ |
| 52 | + async getId(): Promise<string|null> { |
| 53 | + return (await this.host()).getProperty('id'); |
| 54 | + } |
| 55 | + |
| 56 | + /** Gets the checked radio-button in a radio-group. */ |
| 57 | + async getCheckedRadioButton(): Promise<MatRadioButtonHarness|null> { |
| 58 | + for (let radioButton of await this.getRadioButtons()) { |
| 59 | + if (await radioButton.isChecked()) { |
| 60 | + return radioButton; |
| 61 | + } |
| 62 | + } |
| 63 | + return null; |
| 64 | + } |
| 65 | + |
| 66 | + /** Gets the checked value of the radio-group. */ |
| 67 | + async getCheckedValue(): Promise<string|null> { |
| 68 | + const checkedRadio = await this.getCheckedRadioButton(); |
| 69 | + if (!checkedRadio) { |
| 70 | + return null; |
| 71 | + } |
| 72 | + return checkedRadio.getValue(); |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Gets a list of radio buttons which are part of the radio-group. |
| 77 | + * @param filter Optionally filters which radio buttons are included. |
| 78 | + */ |
| 79 | + async getRadioButtons(filter: RadioButtonHarnessFilters = {}): Promise<MatRadioButtonHarness[]> { |
| 80 | + return this.locatorForAll(MatRadioButtonHarness.with(filter))(); |
| 81 | + } |
| 82 | + |
| 83 | + /** |
| 84 | + * Checks a radio button in this group. |
| 85 | + * @param filter An optional filter to apply to the child radio buttons. The first tab matching |
| 86 | + * the filter will be selected. |
| 87 | + */ |
| 88 | + async checkRadioButton(filter: RadioButtonHarnessFilters = {}): Promise<void> { |
| 89 | + const radioButtons = await this.getRadioButtons(filter); |
| 90 | + if (!radioButtons.length) { |
| 91 | + throw Error(`Could not find radio button matching ${JSON.stringify(filter)}`); |
| 92 | + } |
| 93 | + return radioButtons[0].check(); |
| 94 | + } |
| 95 | + |
| 96 | + /** Gets the name attribute of the host element. */ |
| 97 | + private async _getGroupNameFromHost() { |
| 98 | + return (await this.host()).getAttribute('name'); |
| 99 | + } |
| 100 | + |
| 101 | + /** Gets a list of the name attributes of all child radio buttons. */ |
| 102 | + private async _getNamesFromRadioButtons(): Promise<string[]> { |
| 103 | + const groupNames: string[] = []; |
| 104 | + for (let radio of await this.getRadioButtons()) { |
| 105 | + const radioName = await radio.getName(); |
| 106 | + if (radioName !== null) { |
| 107 | + groupNames.push(radioName); |
| 108 | + } |
| 109 | + } |
| 110 | + return groupNames; |
| 111 | + } |
| 112 | + |
| 113 | + /** Checks if the specified radio names are all equal. */ |
| 114 | + private _checkRadioNamesInGroupEqual(radioNames: string[]): boolean { |
| 115 | + let groupName: string|null = null; |
| 116 | + for (let radioName of radioNames) { |
| 117 | + if (groupName === null) { |
| 118 | + groupName = radioName; |
| 119 | + } else if (groupName !== radioName) { |
| 120 | + return false; |
| 121 | + } |
| 122 | + } |
| 123 | + return true; |
| 124 | + } |
| 125 | + |
| 126 | + /** |
| 127 | + * Checks if a radio-group harness has the given name. Throws if a radio-group with |
| 128 | + * matching name could be found but has mismatching radio-button names. |
| 129 | + */ |
| 130 | + private static async _checkRadioGroupName(harness: MatRadioGroupHarness, name: string) { |
| 131 | + // Check if there is a radio-group which has the "name" attribute set |
| 132 | + // to the expected group name. It's not possible to always determine |
| 133 | + // the "name" of a radio-group by reading the attribute. This is because |
| 134 | + // the radio-group does not set the "name" as an element attribute if the |
| 135 | + // "name" value is set through a binding. |
| 136 | + if (await harness._getGroupNameFromHost() === name) { |
| 137 | + return true; |
| 138 | + } |
| 139 | + // Check if there is a group with radio-buttons that all have the same |
| 140 | + // expected name. This implies that the group has the given name. It's |
| 141 | + // not possible to always determine the name of a radio-group through |
| 142 | + // the attribute because there is |
| 143 | + const radioNames = await harness._getNamesFromRadioButtons(); |
| 144 | + if (radioNames.indexOf(name) === -1) { |
| 145 | + return false; |
| 146 | + } |
| 147 | + if (!harness._checkRadioNamesInGroupEqual(radioNames)) { |
| 148 | + throw Error( |
| 149 | + `The locator found a radio-group with name "${name}", but some ` + |
| 150 | + `radio-button's within the group have mismatching names, which is invalid.`); |
| 151 | + } |
| 152 | + return true; |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +/** Harness for interacting with an MDC-based mat-radio-button in tests. */ |
| 157 | +export class MatRadioButtonHarness extends ComponentHarness { |
| 158 | + /** The selector for the host element of a `MatRadioButton` instance. */ |
| 159 | + static hostSelector = '.mat-mdc-radio-button'; |
| 160 | + |
| 161 | + /** |
| 162 | + * Gets a `HarnessPredicate` that can be used to search for a `MatRadioButtonHarness` that meets |
| 163 | + * certain criteria. |
| 164 | + * @param options Options for filtering which radio button instances are considered a match. |
| 165 | + * @return a `HarnessPredicate` configured with the given options. |
| 166 | + */ |
| 167 | + static with(options: RadioButtonHarnessFilters = {}): HarnessPredicate<MatRadioButtonHarness> { |
| 168 | + return new HarnessPredicate(MatRadioButtonHarness, options) |
| 169 | + .addOption( |
| 170 | + 'label', options.label, |
| 171 | + (harness, label) => HarnessPredicate.stringMatches(harness.getLabelText(), label)) |
| 172 | + .addOption( |
| 173 | + 'name', options.name, async (harness, name) => (await harness.getName()) === name); |
| 174 | + } |
| 175 | + |
| 176 | + private _label = this.locatorFor('label'); |
| 177 | + private _input = this.locatorFor('input'); |
| 178 | + |
| 179 | + /** Whether the radio-button is checked. */ |
| 180 | + async isChecked(): Promise<boolean> { |
| 181 | + const checked = (await this._input()).getProperty('checked'); |
| 182 | + return coerceBooleanProperty(await checked); |
| 183 | + } |
| 184 | + |
| 185 | + /** Whether the radio-button is disabled. */ |
| 186 | + async isDisabled(): Promise<boolean> { |
| 187 | + const disabled = (await this._input()).getAttribute('disabled'); |
| 188 | + return coerceBooleanProperty(await disabled); |
| 189 | + } |
| 190 | + |
| 191 | + /** Whether the radio-button is required. */ |
| 192 | + async isRequired(): Promise<boolean> { |
| 193 | + const required = (await this._input()).getAttribute('required'); |
| 194 | + return coerceBooleanProperty(await required); |
| 195 | + } |
| 196 | + |
| 197 | + /** Gets the radio-button's name. */ |
| 198 | + async getName(): Promise<string|null> { |
| 199 | + return (await this._input()).getAttribute('name'); |
| 200 | + } |
| 201 | + |
| 202 | + /** Gets the radio-button's id. */ |
| 203 | + async getId(): Promise<string|null> { |
| 204 | + return (await this.host()).getProperty('id'); |
| 205 | + } |
| 206 | + |
| 207 | + /** |
| 208 | + * Gets the value of the radio-button. The radio-button value will be converted to a string. |
| 209 | + * |
| 210 | + * Note: This means that for radio-button's with an object as a value `[object Object]` is |
| 211 | + * intentionally returned. |
| 212 | + */ |
| 213 | + async getValue(): Promise<string|null> { |
| 214 | + return (await this._input()).getProperty('value'); |
| 215 | + } |
| 216 | + |
| 217 | + /** Gets the radio-button's label text. */ |
| 218 | + async getLabelText(): Promise<string> { |
| 219 | + return (await this._label()).text(); |
| 220 | + } |
| 221 | + |
| 222 | + /** Focuses the radio-button. */ |
| 223 | + async focus(): Promise<void> { |
| 224 | + return (await this._input()).focus(); |
| 225 | + } |
| 226 | + |
| 227 | + /** Blurs the radio-button. */ |
| 228 | + async blur(): Promise<void> { |
| 229 | + return (await this._input()).blur(); |
| 230 | + } |
| 231 | + |
| 232 | + /** Whether the radio-button is focused. */ |
| 233 | + async isFocused(): Promise<boolean> { |
| 234 | + return (await this._input()).isFocused(); |
| 235 | + } |
| 236 | + |
| 237 | + /** |
| 238 | + * Puts the radio-button in a checked state by clicking it if it is currently unchecked, |
| 239 | + * or doing nothing if it is already checked. |
| 240 | + */ |
| 241 | + async check(): Promise<void> { |
| 242 | + if (!(await this.isChecked())) { |
| 243 | + return (await this._label()).click(); |
| 244 | + } |
| 245 | + } |
| 246 | +} |
0 commit comments