diff --git a/apps/example-app-karma/src/app/issues/issue-222.spec.ts b/apps/example-app-karma/src/app/issues/issue-222.spec.ts
index 740d8184..b6ac5204 100644
--- a/apps/example-app-karma/src/app/issues/issue-222.spec.ts
+++ b/apps/example-app-karma/src/app/issues/issue-222.spec.ts
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/angular';
-it('https://github.com/testing-library/angular-testing-library/issues/222', async () => {
+it('https://github.com/testing-library/angular-testing-library/issues/222 with rerender', async () => {
const { rerender } = await render(`
Hello {{ name}}
`, {
componentProperties: {
name: 'Sarah',
@@ -8,7 +8,21 @@ it('https://github.com/testing-library/angular-testing-library/issues/222', asyn
});
expect(screen.getByText('Hello Sarah')).toBeTruthy();
- rerender({ name: 'Mark' });
+
+ await rerender({ name: 'Mark' });
+
+ expect(screen.getByText('Hello Mark')).toBeTruthy();
+});
+
+it('https://github.com/testing-library/angular-testing-library/issues/222 with change', async () => {
+ const { change } = await render(`Hello {{ name}}
`, {
+ componentProperties: {
+ name: 'Sarah',
+ },
+ });
+
+ expect(screen.getByText('Hello Sarah')).toBeTruthy();
+ await change({ name: 'Mark' });
expect(screen.getByText('Hello Mark')).toBeTruthy();
});
diff --git a/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts b/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts
index 28d736c6..1d87edf9 100644
--- a/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts
+++ b/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts
@@ -10,16 +10,29 @@ test('should run logic in the input setter and getter', async () => {
expect(getterValueControl).toHaveTextContent('I am value from getter Angular');
});
-test('should run logic in the input setter and getter while re-rendering', async () => {
- const { rerender } = await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
+test('should run logic in the input setter and getter while changing', async () => {
+ const { change } = await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
const valueControl = screen.getByTestId('value');
const getterValueControl = screen.getByTestId('value-getter');
expect(valueControl).toHaveTextContent('I am value from setter Angular');
expect(getterValueControl).toHaveTextContent('I am value from getter Angular');
- await rerender({ value: 'React' });
+ await change({ value: 'React' });
expect(valueControl).toHaveTextContent('I am value from setter React');
expect(getterValueControl).toHaveTextContent('I am value from getter React');
});
+
+test('should run logic in the input setter and getter while re-rendering', async () => {
+ const { rerender } = await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
+
+ expect(screen.getByTestId('value')).toHaveTextContent('I am value from setter Angular');
+ expect(screen.getByTestId('value-getter')).toHaveTextContent('I am value from getter Angular');
+
+ await rerender({ value: 'React' });
+
+ // note we have to re-query because the elements are not the same anymore
+ expect(screen.getByTestId('value')).toHaveTextContent('I am value from setter React');
+ expect(screen.getByTestId('value-getter')).toHaveTextContent('I am value from getter React');
+});
diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts
index 54b5f2ee..bda39543 100644
--- a/projects/testing-library/src/lib/models.ts
+++ b/projects/testing-library/src/lib/models.ts
@@ -54,9 +54,16 @@ export interface RenderResult extend
navigate: (elementOrPath: Element | string, basePath?: string) => Promise;
/**
* @description
- * Re-render the same component with different props.
+ * Re-render the same component with different properties.
+ * This creates a new instance of the component.
*/
- rerender: (componentProperties: Partial) => void;
+ rerender: (rerenderedProperties: Partial) => void;
+
+ /**
+ * @description
+ * Keeps the current fixture intact and invokes ngOnChanges with the updated properties.
+ */
+ change: (changedProperties: Partial) => void;
}
export interface RenderComponentOptions {
diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts
index 36ee871b..bfbe7f93 100644
--- a/projects/testing-library/src/lib/testing-library.ts
+++ b/projects/testing-library/src/lib/testing-library.ts
@@ -97,53 +97,30 @@ export async function render(
schemas: [...schemas],
});
- if (componentProviders) {
- componentProviders
- .reduce((acc, provider) => acc.concat(provider), [])
- .forEach((p) => {
- const { provide, ...provider } = p;
- TestBed.overrideProvider(provide, provider);
- });
- }
-
- const fixture = await createComponentFixture(sut, { template, wrapper });
- setComponentProperties(fixture, { componentProperties });
-
- if (removeAngularAttributes) {
- fixture.nativeElement.removeAttribute('ng-version');
- const idAttribute = fixture.nativeElement.getAttribute('id');
- if (idAttribute && idAttribute.startsWith('root')) {
- fixture.nativeElement.removeAttribute('id');
- }
- }
-
- mountedFixtures.add(fixture);
-
await TestBed.compileComponents();
- let isAlive = true;
- fixture.componentRef.onDestroy(() => (isAlive = false));
+ componentProviders
+ .reduce((acc, provider) => acc.concat(provider), [])
+ .forEach((p) => {
+ const { provide, ...provider } = p;
+ TestBed.overrideProvider(provide, provider);
+ });
- function detectChanges() {
- if (isAlive) {
- fixture.detectChanges();
- }
- }
+ const componentContainer = createComponentFixture(sut, { template, wrapper });
- // Call ngOnChanges on initial render
- if (hasOnChangesHook(fixture.componentInstance)) {
- const changes = getChangesObj(null, componentProperties);
- fixture.componentInstance.ngOnChanges(changes);
- }
+ let fixture: ComponentFixture;
+ let detectChanges: () => void;
- if (detectChangesOnRender) {
- detectChanges();
- }
+ await renderFixture(componentProperties);
+
+ const rerender = async (rerenderedProperties: Partial) => {
+ await renderFixture(rerenderedProperties);
+ };
- const rerender = (rerenderedProperties: Partial) => {
- const changes = getChangesObj(fixture.componentInstance, rerenderedProperties);
+ const change = (changedProperties: Partial) => {
+ const changes = getChangesObj(fixture.componentInstance, changedProperties);
- setComponentProperties(fixture, { componentProperties: rerenderedProperties });
+ setComponentProperties(fixture, { componentProperties: changedProperties });
if (hasOnChangesHook(fixture.componentInstance)) {
fixture.componentInstance.ngOnChanges(changes);
@@ -192,9 +169,10 @@ export async function render(
return {
fixture,
- detectChanges,
+ detectChanges: () => detectChanges(),
navigate,
rerender,
+ change,
debugElement: typeof sut === 'string' ? fixture.debugElement : fixture.debugElement.query(By.directive(sut)),
container: fixture.nativeElement,
debug: (element = fixture.nativeElement, maxLength, options) =>
@@ -203,6 +181,42 @@ export async function render(
: console.log(dtlPrettyDOM(element, maxLength, options)),
...replaceFindWithFindAndDetectChanges(dtlGetQueriesForElement(fixture.nativeElement, queries)),
};
+
+ async function renderFixture(properties: Partial) {
+ if (fixture) {
+ cleanupAtFixture(fixture);
+ }
+
+ fixture = await createComponent(componentContainer);
+ setComponentProperties(fixture, { componentProperties: properties });
+
+ if (removeAngularAttributes) {
+ fixture.nativeElement.removeAttribute('ng-version');
+ const idAttribute = fixture.nativeElement.getAttribute('id');
+ if (idAttribute && idAttribute.startsWith('root')) {
+ fixture.nativeElement.removeAttribute('id');
+ }
+ }
+ mountedFixtures.add(fixture);
+
+ let isAlive = true;
+ fixture.componentRef.onDestroy(() => (isAlive = false));
+
+ if (hasOnChangesHook(fixture.componentInstance)) {
+ const changes = getChangesObj(null, componentProperties);
+ fixture.componentInstance.ngOnChanges(changes);
+ }
+
+ detectChanges = () => {
+ if (isAlive) {
+ fixture.detectChanges();
+ }
+ };
+
+ if (detectChangesOnRender) {
+ detectChanges();
+ }
+ }
}
async function createComponent(component: Type): Promise> {
@@ -211,19 +225,19 @@ async function createComponent(component: Type): Promise(
+function createComponentFixture(
sut: Type | string,
{ template, wrapper }: Pick, 'template' | 'wrapper'>,
-): Promise> {
+): Type {
if (typeof sut === 'string') {
TestBed.overrideTemplate(wrapper, sut);
- return createComponent(wrapper);
+ return wrapper;
}
if (template) {
TestBed.overrideTemplate(wrapper, template);
- return createComponent(wrapper);
+ return wrapper;
}
- return createComponent(sut);
+ return sut;
}
function setComponentProperties(
diff --git a/projects/testing-library/tests/change.spec.ts b/projects/testing-library/tests/change.spec.ts
new file mode 100644
index 00000000..85cc3667
--- /dev/null
+++ b/projects/testing-library/tests/change.spec.ts
@@ -0,0 +1,85 @@
+import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { render, screen } from '../src/public_api';
+
+@Component({
+ selector: 'atl-fixture',
+ template: ` {{ firstName }} {{ lastName }} `,
+})
+class FixtureComponent {
+ @Input() firstName = 'Sarah';
+ @Input() lastName;
+}
+
+test('changes the component with updated props', async () => {
+ const { change } = await render(FixtureComponent);
+ expect(screen.getByText('Sarah')).toBeInTheDocument();
+
+ const firstName = 'Mark';
+ change({ firstName });
+
+ expect(screen.getByText(firstName)).toBeInTheDocument();
+});
+
+test('changes the component with updated props while keeping other props untouched', async () => {
+ const firstName = 'Mark';
+ const lastName = 'Peeters';
+ const { change } = await render(FixtureComponent, {
+ componentProperties: {
+ firstName,
+ lastName,
+ },
+ });
+
+ expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
+
+ const firstName2 = 'Chris';
+ change({ firstName: firstName2 });
+
+ expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
+});
+
+@Component({
+ selector: 'atl-fixture',
+ template: ` {{ name }} `,
+})
+class FixtureWithNgOnChangesComponent implements OnChanges {
+ @Input() name = 'Sarah';
+ @Input() nameChanged: (name: string, isFirstChange: boolean) => void;
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.name && this.nameChanged) {
+ this.nameChanged(changes.name.currentValue, changes.name.isFirstChange());
+ }
+ }
+}
+
+test('will call ngOnChanges on change', async () => {
+ const nameChanged = jest.fn();
+ const componentProperties = { nameChanged };
+ const { change } = await render(FixtureWithNgOnChangesComponent, { componentProperties });
+ expect(screen.getByText('Sarah')).toBeInTheDocument();
+
+ const name = 'Mark';
+ change({ name });
+
+ expect(screen.getByText(name)).toBeInTheDocument();
+ expect(nameChanged).toHaveBeenCalledWith(name, false);
+});
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'atl-fixture',
+ template: ` Number
`,
+})
+class FixtureWithOnPushComponent {
+ @Input() activeField: string;
+}
+
+test('update properties on change', async () => {
+ const { change } = await render(FixtureWithOnPushComponent);
+ const numberHtmlElementRef = screen.queryByTestId('number');
+
+ expect(numberHtmlElementRef).not.toHaveClass('active');
+ change({ activeField: 'number' });
+ expect(numberHtmlElementRef).toHaveClass('active');
+});
diff --git a/projects/testing-library/tests/rerender.spec.ts b/projects/testing-library/tests/rerender.spec.ts
index c98bd1bc..443560a2 100644
--- a/projects/testing-library/tests/rerender.spec.ts
+++ b/projects/testing-library/tests/rerender.spec.ts
@@ -1,66 +1,40 @@
-import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { Component, Input } from '@angular/core';
import { render, screen } from '../src/public_api';
@Component({
selector: 'atl-fixture',
- template: ` {{ name }} `,
+ template: ` {{ firstName }} {{ lastName }} `,
})
class FixtureComponent {
- @Input() name = 'Sarah';
+ @Input() firstName = 'Sarah';
+ @Input() lastName;
}
-test('will rerender the component with updated props', async () => {
+test('rerenders the component with updated props', async () => {
const { rerender } = await render(FixtureComponent);
expect(screen.getByText('Sarah')).toBeInTheDocument();
- const name = 'Mark';
- rerender({ name });
+ const firstName = 'Mark';
+ await rerender({ firstName });
- expect(screen.getByText(name)).toBeInTheDocument();
+ expect(screen.getByText(firstName)).toBeInTheDocument();
});
-@Component({
- selector: 'atl-fixture',
- template: ` {{ name }} `,
-})
-class FixtureWithNgOnChangesComponent implements OnChanges {
- @Input() name = 'Sarah';
- @Input() nameChanged: (name: string, isFirstChange: boolean) => void;
-
- ngOnChanges(changes: SimpleChanges) {
- if (changes.name && this.nameChanged) {
- this.nameChanged(changes.name.currentValue, changes.name.isFirstChange());
- }
- }
-}
-
-test('will call ngOnChanges on rerender', async () => {
- const nameChanged = jest.fn();
- const componentProperties = { nameChanged };
- const { rerender } = await render(FixtureWithNgOnChangesComponent, { componentProperties });
- expect(screen.getByText('Sarah')).toBeInTheDocument();
+test('rerenders the component with updated props and resets other props', async () => {
+ const firstName = 'Mark';
+ const lastName = 'Peeters';
+ const { rerender } = await render(FixtureComponent, {
+ componentProperties: {
+ firstName,
+ lastName,
+ },
+ });
- const name = 'Mark';
- rerender({ name });
-
- expect(screen.getByText(name)).toBeInTheDocument();
- expect(nameChanged).toHaveBeenCalledWith(name, false);
-});
-
-@Component({
- changeDetection: ChangeDetectionStrategy.OnPush,
- selector: 'atl-fixture',
- template: ` Number
`,
-})
-class FixtureWithOnPushComponent {
- @Input() activeField: string;
-}
+ expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
-test('update properties on rerender', async () => {
- const { rerender } = await render(FixtureWithOnPushComponent);
- const numberHtmlElementRef = screen.queryByTestId('number');
+ const firstName2 = 'Chris';
+ rerender({ firstName: firstName2 });
- expect(numberHtmlElementRef).not.toHaveClass('active');
- rerender({ activeField: 'number' });
- expect(numberHtmlElementRef).toHaveClass('active');
+ expect(screen.queryByText(`${firstName2} ${lastName}`)).not.toBeInTheDocument();
+ expect(screen.queryByText(firstName2)).not.toBeInTheDocument();
});