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

feat(components/toast): add toast and toaster harness #3141

Merged
merged 11 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from 10 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SkyToastType } from '@skyux/toast';
import { SkyToasterHarness } from '@skyux/toast/testing';

import { DemoComponent } from './demo.component';

describe('Basic toast demo', () => {
let fixture: ComponentFixture<DemoComponent>;
let loader: HarnessLoader;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [DemoComponent, NoopAnimationsModule],
});

fixture = TestBed.createComponent(DemoComponent);
loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
});

async function setupTest(): Promise<{
toasterHarness: SkyToasterHarness;
}> {
fixture.componentInstance.openToast();
fixture.detectChanges();

const toasterHarness: SkyToasterHarness =
await loader.getHarness(SkyToasterHarness);

return { toasterHarness };
}

it('should open success toasts', async () => {
const { toasterHarness } = await setupTest();

fixture.componentInstance.openToast();
fixture.detectChanges();

await expectAsync(toasterHarness.getNumberOfToasts()).toBeResolvedTo(2);

const toast = await toasterHarness.getToastByMessage(
'This is a sample toast message.',
);

await expectAsync(toast.getType()).toBeResolvedTo(SkyToastType.Success);
});

it('should close all toasts', async () => {
fixture.componentInstance.openToast();
fixture.detectChanges();
fixture.componentInstance.closeAll();
fixture.detectChanges();

await expectAsync(loader.getHarness(SkyToasterHarness)).toBeRejected();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { SkyToastService, SkyToastType } from '@skyux/toast';
export class DemoComponent {
readonly #toastSvc = inject(SkyToastService);

protected openToast(): void {
public openToast(): void {
this.#toastSvc.openMessage('This is a sample toast message.', {
type: SkyToastType.Success,
});
}

protected closeAll(): void {
public closeAll(): void {
this.#toastSvc.closeAll();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ComponentHarness } from '@angular/cdk/testing';

export class CustomToastHarness extends ComponentHarness {
public static hostSelector = 'app-toast-content-demo';

public async getText(): Promise<string> {
return await (await this.host()).text();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SkyToastType } from '@skyux/toast';
import { SkyToasterHarness } from '@skyux/toast/testing';

import { CustomToastHarness } from './custom-toast-harness';
import { DemoComponent } from './demo.component';

describe('Custom component toast demo', () => {
let fixture: ComponentFixture<DemoComponent>;
let loader: HarnessLoader;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [DemoComponent, NoopAnimationsModule],
});

fixture = TestBed.createComponent(DemoComponent);
loader = TestbedHarnessEnvironment.documentRootLoader(fixture);
});

async function setupTest(): Promise<{
toasterHarness: SkyToasterHarness;
}> {
fixture.componentInstance.openToast();
fixture.detectChanges();

const toasterHarness: SkyToasterHarness =
await loader.getHarness(SkyToasterHarness);

return { toasterHarness };
}

it('should open custom component in toast', async () => {
const { toasterHarness } = await setupTest();

const toasts = await toasterHarness.getToasts();
const customHarness = await toasts[0].queryHarness(CustomToastHarness);

await expectAsync(toasts[0].getType()).toBeResolvedTo(SkyToastType.Success);
await expectAsync(customHarness.getText()).toBeResolvedTo(
'Custom message: This toast has embedded a custom component for its content.',
);
});

it('should close all toasts', async () => {
fixture.componentInstance.openToast();
fixture.detectChanges();
fixture.componentInstance.closeAll();
fixture.detectChanges();

await expectAsync(loader.getHarness(SkyToasterHarness)).toBeRejected();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { CustomToastComponent } from './custom-toast.component';
export class DemoComponent {
readonly #toastSvc = inject(SkyToastService);

protected openToast(): void {
public openToast(): void {
const context = new CustomToastContext(
'This toast has embedded a custom component for its content.',
);
Expand All @@ -31,7 +31,7 @@ export class DemoComponent {
);
}

protected closeAll(): void {
public closeAll(): void {
this.#toastSvc.closeAll();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SkyHarnessFilters } from '../../shared/harness-filters';
import { BaseHarnessFilters } from '@angular/cdk/testing';

/**
* A set of criteria that can be used to filter a list of SkyOverlayHarness instances.
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type
export interface SkyOverlayHarnessFilters extends SkyHarnessFilters {}
export interface SkyOverlayHarnessFilters extends BaseHarnessFilters {}
1 change: 1 addition & 0 deletions libs/components/toast/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"homepage": "https://github.com/blackbaud/skyux#readme",
"peerDependencies": {
"@angular/animations": "^18.2.13",
"@angular/cdk": "^18.2.14",
"@angular/common": "^18.2.13",
"@angular/core": "^18.2.13",
"@angular/platform-browser": "^18.2.13",
Expand Down
2 changes: 1 addition & 1 deletion libs/components/toast/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"dependsOn": [
"^build",
{
"projects": ["testing"],
"projects": ["core", "testing"],
"target": "build"
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SkyToastType } from './toast-type';
*/
export interface SkyToastConfig {
/**
* The `SkyToastType` type that determines the color and icon for the toast. This property defaults to `SkyToastType.Info`.
* The `SkyToastType` type that determines the color and icon for the toast. This property defaults to `Info`.
*/
type?: SkyToastType;

Expand Down
12 changes: 12 additions & 0 deletions libs/components/toast/testing/src/modules/toast-harness-filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BaseHarnessFilters } from '@angular/cdk/testing';

/**
* A set of criteria that can be used to filter a list of `SkyToastHarness` instances.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type
export interface SkyToastHarnessFilters extends BaseHarnessFilters {
/**
* Finds toasts with the matching text.
*/
message?: string;
}
72 changes: 72 additions & 0 deletions libs/components/toast/testing/src/modules/toast-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { HarnessPredicate } from '@angular/cdk/testing';
import { SkyQueryableComponentHarness } from '@skyux/core/testing';
import { SkyToastType } from '@skyux/toast';

import { SkyToastHarnessFilters } from './toast-harness-filters';

/**
* Harness for interacting with the toast component in tests.
*/
export class SkyToastHarness extends SkyQueryableComponentHarness {
/**
* @internal
*/
public static hostSelector = 'sky-toast';

#getToast = this.locatorFor('.sky-toast');

/**
* Gets a `HarnessPredicate` that can be used to search for a
* `SkyToastHarness` that meets certain criteria.
*/
public static with(
filters: SkyToastHarnessFilters,
): HarnessPredicate<SkyToastHarness> {
return new HarnessPredicate(SkyToastHarness, filters).addOption(
'message',
filters.message,
async (harness, message) => {
const harnessMessage = await harness.getMessage();
return await HarnessPredicate.stringMatches(message, harnessMessage);
},
);
}

/**
* Clicks the toast close button.
*/
public async close(): Promise<void> {
const button = await this.locatorFor('.sky-toast-btn-close')();
return await button.click();
}

/**
* Gets the toast message.
*/
public async getMessage(): Promise<string> {
const toastBody = await this.locatorForOptional('.sky-toast-body')();

if (toastBody) {
return (await toastBody.text()).trim();
} else {
throw new Error(
'No toast message found. This method cannot be used to query toasts with custom components.',
);
}
}

/**
* Gets the toast type.
*/
public async getType(): Promise<SkyToastType> {
const toast = await this.#getToast();
if (await toast.hasClass('sky-toast-danger')) {
return SkyToastType.Danger;
} else if (await toast.hasClass('sky-toast-warning')) {
return SkyToastType.Warning;
} else if (await toast.hasClass('sky-toast-success')) {
return SkyToastType.Success;
}
return SkyToastType.Info;
}
}
Loading
Loading