Skip to content

fix(material/button): harness not picking up buttons with dynamic appearance #31327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 12, 2025
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
35 changes: 23 additions & 12 deletions src/material/button/testing/button-harness.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Component} from '@angular/core';
import {Component, signal} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {Platform} from '@angular/cdk/platform';
import {HarnessLoader, parallel} from '@angular/cdk/testing';
Expand All @@ -7,22 +7,21 @@ import {MatButtonModule} from '../module';
import {MatIconModule} from '../../icon';
import {MatIconHarness} from '../../icon/testing';
import {MatButtonHarness} from './button-harness';
import {MatButtonAppearance} from '../button-base';

describe('MatButtonHarness', () => {
let fixture: ComponentFixture<ButtonHarnessTest>;
let loader: HarnessLoader;
let platform: Platform;

beforeEach(() => {
fixture = TestBed.createComponent(ButtonHarnessTest);
fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture);
platform = TestBed.inject(Platform);
});

it('should load all button harnesses', async () => {
const buttons = await loader.getAllHarnesses(MatButtonHarness);
expect(buttons.length).toBe(16);
expect(buttons.length).toBe(17);
});

it('should load button with exact text', async () => {
Expand All @@ -41,7 +40,7 @@ describe('MatButtonHarness', () => {
it('should filter by whether a button is disabled', async () => {
const enabledButtons = await loader.getAllHarnesses(MatButtonHarness.with({disabled: false}));
const disabledButtons = await loader.getAllHarnesses(MatButtonHarness.with({disabled: true}));
expect(enabledButtons.length).toBe(14);
expect(enabledButtons.length).toBe(15);
expect(disabledButtons.length).toBe(2);
});

Expand Down Expand Up @@ -79,7 +78,7 @@ describe('MatButtonHarness', () => {
const button = await loader.getHarness(MatButtonHarness.with({text: 'Basic button'}));
await button.click();

expect(fixture.componentInstance.clicked).toBe(true);
expect(fixture.componentInstance.clicked()).toBe(true);
});

it('should not click a disabled button', async () => {
Expand All @@ -88,14 +87,14 @@ describe('MatButtonHarness', () => {
// cancel dispatched click events on disabled buttons. We skip this check on Edge and Firefox.
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1582570 and:
// https://stackoverflow.com/questions/32377026/disabled-button-is-clickable-on-edge-browser
if (platform.FIREFOX) {
if (TestBed.inject(Platform).FIREFOX) {
return;
}

const button = await loader.getHarness(MatButtonHarness.with({text: 'Filled button'}));
await button.click();

expect(fixture.componentInstance.clicked).toBe(false);
expect(fixture.componentInstance.clicked()).toBe(false);
});

it('should be able to handle nested harnesses', async () => {
Expand Down Expand Up @@ -127,6 +126,7 @@ describe('MatButtonHarness', () => {
'basic',
'basic',
'basic',
'basic',
'icon',
'fab',
'mini-fab',
Expand All @@ -151,6 +151,7 @@ describe('MatButtonHarness', () => {
'filled',
'elevated',
'outlined',
'tonal',
null,
null,
null,
Expand All @@ -166,16 +167,25 @@ describe('MatButtonHarness', () => {
const button = await loader.getHarness(MatButtonHarness.with({appearance: 'filled'}));
expect(await button.getText()).toBe('Filled button');
});

it('should get the appearance of a button with a dynamic appearance', async () => {
const button = await loader.getHarness(
MatButtonHarness.with({selector: '#dynamic-appearance'}),
);
expect(await button.getAppearance()).toBe('tonal');
fixture.componentInstance.dynamicAppearance.set('filled');
expect(await button.getAppearance()).toBe('filled');
});
});

@Component({
// Include one of each type of button selector to ensure that they're all captured by
// the harness's selector.
template: `
<button id="basic" type="button" matButton (click)="clicked = true">
<button id="basic" type="button" matButton (click)="clicked.set(true)">
Basic button
</button>
<button id="flat" type="button" matButton="filled" disabled (click)="clicked = true">
<button id="flat" type="button" matButton="filled" disabled (click)="clicked.set(true)">
Filled button
</button>
<button id="raised" type="button" matButton="elevated">Elevated button</button>
Expand All @@ -194,13 +204,14 @@ describe('MatButtonHarness', () => {
<a id="anchor-flat" matButton="filled">Filled anchor</a>
<a id="anchor-raised" matButton="elevated" disabled>Elevated anchor</a>
<a id="anchor-stroked" matButton="outlined">Stroked anchor</a>
<a id="dynamic-appearance" [matButton]="dynamicAppearance()">Stroked anchor</a>
<a id="anchor-icon" matIconButton>Icon anchor</a>
<a id="anchor-fab" matFab>Fab anchor</a>
<a id="anchor-mini-fab" matMiniFab>Mini Fab anchor</a>
`,
imports: [MatButtonModule, MatIconModule],
})
class ButtonHarnessTest {
disabled = true;
clicked = false;
clicked = signal(false);
dynamicAppearance = signal<MatButtonAppearance>('tonal');
}
12 changes: 8 additions & 4 deletions src/material/button/testing/button-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ import {ButtonAppearance, ButtonHarnessFilters, ButtonVariant} from './button-ha

/** Harness for interacting with a mat-button in tests. */
export class MatButtonHarness extends ContentContainerComponentHarness {
// TODO(jelbourn) use a single class, like `.mat-button-base`
static hostSelector = `[matButton], [mat-button], [matIconButton], [matFab], [matMiniFab],
[mat-raised-button], [mat-flat-button], [mat-icon-button], [mat-stroked-button], [mat-fab],
[mat-mini-fab]`;
// Note: `.mat-mdc-button-base` should be enough for all buttons, however some apps are using
// the harness without actually having an applied button. Keep the attributes for backwards
// compatibility.

/** Selector for the harness. */
static hostSelector = `.mat-mdc-button-base, [matButton], [mat-button], [matIconButton],
[matFab], [matMiniFab], [mat-raised-button], [mat-flat-button], [mat-icon-button],
[mat-stroked-button], [mat-fab], [mat-mini-fab]`;

/**
* Gets a `HarnessPredicate` that can be used to search for a button with specific attributes.
Expand Down
Loading