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/core): add SkyMediaQueryController to interact with breakpoints in tests #2802

Open
wants to merge 96 commits into
base: main
Choose a base branch
from

Conversation

Blackbaud-SteveBrush
Copy link
Member

@Blackbaud-SteveBrush Blackbaud-SteveBrush commented Oct 4, 2024

AB#2195494

Overview of API

Testing

Consumers can add the following to their tests to interact with breakpoints:

import { provideSkyMediaQueryTesting } from '@skyux/core/testing';

TestBed.configureTestingModule({
  providers: [provideSkyMediaQueryTesting()]
});

const mediaQueryController = TestBed.inject(SkyMediaQueryTestingController);

mediaQueryController.setBreakpoint('xs');
await mediaQueryController.expectBreakpoint('xs');

Creating responsive hosts

Consumers can apply a container breakpoint observer to an element in their template like this:

<div #responsiveHost="skyResponsiveHost" skyResponsiveHost>
  <p>Breakpoint within container: {{ responsiveHost.breakpointChange | async }}</p>
</div>

Or on their component's host element:

@Component({
  hostDirectives: [SkyResponsiveHostDirective],
  standalone: true,
  styles: `
    :host {
      display: block;
    }
  `,
  template: ` <p>Breakpoint within container: {{ breakpoint() }}</p> `,
})
export class DemoComponent {
  protected breakpoint = toSignal(
    inject(SkyMediaQueryService).breakpointChange,
  );
}

For embedded views, the responsive host can pass its injector:

<div #responsiveHost="skyResponsiveHost" skyResponsiveHost>
  <ng-container
    [ngTemplateOutlet]="myTemplate"
    [ngTemplateOutletInjector]="responsiveHost.injector"
</div>

Copy link

nx-cloud bot commented Oct 4, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 5529153. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this CI Pipeline Execution


✅ Successfully ran 2 targets

Sent with 💌 from NxCloud.

@Blackbaud-SteveBrush Blackbaud-SteveBrush added the risk level (author): 1 No additional bugs expected from this change label Oct 4, 2024
@Blackbaud-ErikaMcVey Blackbaud-ErikaMcVey marked this pull request as draft October 7, 2024 17:09
it('should change the breakpoint', async () => {
const { fixture, mediaController } = setupTest();

const containerWithHostDirective = fixture.debugElement.query(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use nativeElement instead of debugElement here per Angular's guidance.

When you're filtering by CSS selector and only testing properties of a browser's native element, the By.css approach might be overkill.

It's often more straightforward and clear to filter with a standard HTMLElement method such as querySelector() or querySelectorAll().

const containerWithHostDirective = (fixture.nativeElement as HTMLElement).querySelector('[data-sky-id="container-w-host-directive"]');

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting; that's new to me.

@@ -145,6 +164,7 @@ export class SkySummaryActionBarComponent implements AfterViewInit, OnDestroy {

public onDirectionChange(direction: string): void {
this.slideDirection = direction;
this.#changeDetector.markForCheck();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is called as a result of an event handler, so in theory markForCheck() should not be needed. Can you make slideDirection a signal?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely.

export function toSkyMediaBreakpoints(
breakpoint: SkyBreakpoint,
): SkyMediaBreakpoints {
const breakpointsMap = new Map<SkyBreakpoint, SkyMediaBreakpoints>([

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't change between calls to toSkyMediaBreakpoints(). Move to the top of this file so it can be reused.

export function toSkyBreakpoint(
breakpoint: SkyMediaBreakpoints,
): SkyBreakpoint {
const breakpointsMap = new Map<SkyMediaBreakpoints, SkyBreakpoint>([

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't change between calls to toSkyBreakpoint(). Move to the top of this file so it can be reused.

import { SkyBreakpointObserver } from './breakpoint-observer';

const QUERIES = new Map<SkyBreakpoint, (width: number) => boolean>([
['xs', (w): boolean => w > 0 && w <= 767],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need the : boolean return type, or can it be inferred from the Map declaration (or array declaration after the suggested refactor above)? Also, I'd make the parameter names match by using width instead of w for readability.

untracked(() => {
if (this.#searchShouldCollapse()) {
if (breakpoint === 'xs') {
this.inputAnimate = INPUT_HIDDEN_STATE;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting properties in an effect() seems like an anti-pattern to me. Can inputAnimate and mobileSearchShown be computed signals instead?

useExisting: SkySplitViewMediaQueryService,
},
],
templateUrl: 'split-view-drawer.component.html',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

templateUrl and styleUrls are missing the ./ prefix. Didn't we hit issues a while back with this? I'm kind of surprised it works. Other split view files also have this problem.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, strange. I updated all the paths.

protected layoutClassName = computed(() => {
const breakpoint = this.#breakpoint();

return untracked(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this use untracked()? it doesn't reference any signals.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misunderstood the use-case; I thought it marked code that doesn't include any signals. Reverted.

effect(() => {
const breakpoint = this.#breakpoint();

untracked(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue with untracked() as above.

breakpoint === 'xs' || breakpoint === 'sm' ? 'single' : 'multi';

if (mode !== this.#mode) {
this.#mode = mode;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another instance of setting a property in an effect. See if this can be a computed signal instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
risk level (author): 4 This change has a high chance of introducing a bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants