Skip to content

Commit

Permalink
feat(ng-dev): added ComponentContextNext.assignWrapperStyles()
Browse files Browse the repository at this point in the history
  • Loading branch information
ersimont committed Jan 5, 2021
1 parent 07cecef commit b3fc051
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 21 deletions.
Expand Up @@ -11,7 +11,7 @@ import {
import { ComponentFixture } from '@angular/core/testing';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSnackBarHarness } from '@angular/material/snack-bar/testing';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserModule, By } from '@angular/platform-browser';
import {
ANIMATION_MODULE_TYPE,
BrowserAnimationsModule,
Expand Down Expand Up @@ -147,6 +147,38 @@ describe('ComponentContextNext', () => {
});
});

describe('.assignWrapperStyles()', () => {
it('can be used before .run()', () => {
const ctx = new ComponentContextNext(TestComponent);
ctx.assignWrapperStyles({ border: '1px solid black' });
ctx.run(() => {
const wrapper = ctx.fixture.debugElement.query(
By.css('.s-libs-dynamic-wrapper'),
);
expect(wrapper.styles).toEqual(
jasmine.objectContaining({ border: '1px solid black' }),
);
});
});

it('changes (only) the passed-in styles', () => {
const ctx = new ComponentContextNext(TestComponent);
ctx.assignWrapperStyles({ border: '1px solid black' });
ctx.run(() => {
ctx.assignWrapperStyles({ 'background-color': 'blue' });
const wrapper = ctx.fixture.debugElement.query(
By.css('.s-libs-dynamic-wrapper'),
);
expect(wrapper.styles).toEqual(
jasmine.objectContaining({
border: '1px solid black',
'background-color': 'blue',
}),
);
});
});
});

describe('.getComponentInstance()', () => {
it('returns the instantiated component', () => {
const ctx = new ComponentContextNext(TestComponent);
Expand Down
Expand Up @@ -16,7 +16,10 @@ import { trimLeftoverStyles } from '../../trim-leftover-styles';
import { extendMetadata } from '../angular-context/angular-context';
import { AngularContextNext } from '../angular-context/angular-context-next';
import { FakeAsyncHarnessEnvironmentNext } from '../angular-context/fake-async-harness-environment-next';
import { createDynamicWrapper } from './create-dynamic-wrapper';
import {
createDynamicWrapper,
WrapperComponent,
} from './create-dynamic-wrapper';

/**
* A superclass to set up testing contexts for components. This is a foundation for an opinionated testing pattern, including everything described in {@link AngularContextNext} plus:
Expand Down Expand Up @@ -148,13 +151,15 @@ export class ComponentContextNext<T> extends AngularContextNext {
/**
* The {@link ComponentFixture} for a synthetic wrapper around your component. Available with the callback to `run()`.
*/
fixture!: ComponentFixture<unknown>;
fixture!: ComponentFixture<WrapperComponent<T>>;

private componentType: Type<T>;
private wrapperType: Type<T>;
private wrapperType: Type<WrapperComponent<T>>;
private inputProperties: Set<keyof T>;
private loader!: HarnessLoader;

private inputs: Partial<T>;
private wrapperStyles: { [klass: string]: any };

/**
* @param componentType `run()` will create a component of this type before running the rest of your test.
Expand All @@ -177,10 +182,32 @@ export class ComponentContextNext<T> extends AngularContextNext {
this.wrapperType = wrapper.type;
this.inputProperties = new Set(wrapper.inputProperties);
this.inputs = {};
this.wrapperStyles = {};
}

/**
* Assign css styles to the div wrapping your component. Can be called before or during `run()`. Accepts an object with the same structure as the {@link https://angular.io/api/common/NgStyle|ngStyle directive}.
*
* ```ts
* ctx.assignWrapperStyles({
* width: '400px',
* height: '600px',
* margin: '20px auto',
* border: '1px solid',
* });
* ```
*/
assignWrapperStyles(styles: { [klass: string]: any }): void {
Object.assign(this.wrapperStyles, styles);

if (this.isInitialized()) {
this.flushStylesToWrapper();
this.tick();
}
}

/**
* Use within `run()` to update the inputs to your component and trigger all the appropriate change detection and lifecycle hooks. Only the inputs specified in `inputs` will be affected.
* Assign inputs passed into your component. Can be called before `run()` to set the initial inputs, or within `run()` to update them and trigger all the appropriate change detection and lifecycle hooks.
*/
assignInputs(inputs: Partial<T>): void {
for (const key of keys(inputs)) {
Expand All @@ -193,7 +220,7 @@ export class ComponentContextNext<T> extends AngularContextNext {

Object.assign(this.inputs, inputs);
if (this.isInitialized()) {
Object.assign(this.fixture.componentInstance, inputs);
this.flushInputsToWrapper();
this.tick();
}
}
Expand Down Expand Up @@ -231,7 +258,8 @@ export class ComponentContextNext<T> extends AngularContextNext {
this.fixture = TestBed.createComponent(this.wrapperType);
this.loader = FakeAsyncHarnessEnvironmentNext.documentRootLoader(this);

Object.assign(this.fixture.componentInstance, this.inputs);
this.flushStylesToWrapper();
this.flushInputsToWrapper();
this.fixture.detectChanges();
this.tick();
}
Expand All @@ -252,4 +280,12 @@ export class ComponentContextNext<T> extends AngularContextNext {
private isInitialized(): boolean {
return !!this.fixture;
}

private flushInputsToWrapper(): void {
Object.assign(this.fixture.componentInstance.inputs, this.inputs);
}

private flushStylesToWrapper(): void {
Object.assign(this.fixture.componentInstance.styles, this.wrapperStyles);
}
}
Expand Up @@ -9,9 +9,15 @@ interface InputMeta<T> {
property: keyof T;
}

/** @hidden */
export interface WrapperComponent<T> {
inputs: Partial<T>;
styles: { [klass: string]: any };
}

/** @hidden */
interface DynamicWrapper<T> {
type: Type<T>;
type: Type<WrapperComponent<T>>;
inputProperties: Array<keyof T>;
}

Expand All @@ -25,20 +31,13 @@ export function createDynamicWrapper<T>(
({ property }) => !unboundInputs.includes(property),
);

const template = `
<div>
<${selector}
${inputMetas
.map(({ binding, property }) => `[${binding}]="${property}"`)
.join(' ')}
></${selector}>
</div>
`;

@Component({ template })
class DynamicWrapperComponent {}
@Component({ template: buildTemplate(selector, inputMetas) })
class DynamicWrapperComponent {
inputs: Partial<T> = {};
styles: { [klass: string]: any } = {};
}

const type = DynamicWrapperComponent as Type<T>;
const type = DynamicWrapperComponent;
const inputProperties = inputMetas.map((meta) => meta.property);
return { type, inputProperties };
}
Expand Down Expand Up @@ -75,6 +74,7 @@ function isValidSelector(selector: string): boolean {

/** @hidden */
function getInputMetas<T>(componentType: Type<T>): Array<InputMeta<T>> {
// I tried making this support inputs with special characters in their names, but it turns out that *Angular* can only support that when using AOT. So our *dynamic* wrapper cannot.
return flatMap(
(componentType as any).propDecorators,
(decorators: any[], property: any) => {
Expand All @@ -88,3 +88,18 @@ function getInputMetas<T>(componentType: Type<T>): Array<InputMeta<T>> {
},
);
}

/** @hidden */
function buildTemplate<T>(
selector: string,
inputMetas: InputMeta<T>[],
): string {
const bindingStrings = inputMetas.map(
({ binding, property }) => `[${binding}]="inputs.${property}"`,
);
return `
<div class="s-libs-dynamic-wrapper" [ngStyle]="styles">
<${selector} ${bindingStrings.join(' ')}></${selector}>
</div>
`;
}

0 comments on commit b3fc051

Please sign in to comment.