Skip to content

Commit 0e5e3c7

Browse files
feat: add rerender method (#257)
BREAKING CHANGE: `rerender` has been renamed to `change`. The `change` method keeps the current fixture intact and invokes `ngOnChanges`. The new `rerender` method destroys the current component and creates a new instance with the updated properties. BEFORE: ```ts const { rerender } = render(...) rerender({...}) ``` AFTER: ```ts const { change } = render(...) change({...}) ```
1 parent 5b4270c commit 0e5e3c7

File tree

6 files changed

+208
-101
lines changed

6 files changed

+208
-101
lines changed
+16-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
import { render, screen } from '@testing-library/angular';
22

3-
it('https://github.com/testing-library/angular-testing-library/issues/222', async () => {
3+
it('https://github.com/testing-library/angular-testing-library/issues/222 with rerender', async () => {
44
const { rerender } = await render(`<div>Hello {{ name}}</div>`, {
55
componentProperties: {
66
name: 'Sarah',
77
},
88
});
99

1010
expect(screen.getByText('Hello Sarah')).toBeTruthy();
11-
rerender({ name: 'Mark' });
11+
12+
await rerender({ name: 'Mark' });
13+
14+
expect(screen.getByText('Hello Mark')).toBeTruthy();
15+
});
16+
17+
it('https://github.com/testing-library/angular-testing-library/issues/222 with change', async () => {
18+
const { change } = await render(`<div>Hello {{ name}}</div>`, {
19+
componentProperties: {
20+
name: 'Sarah',
21+
},
22+
});
23+
24+
expect(screen.getByText('Hello Sarah')).toBeTruthy();
25+
await change({ name: 'Mark' });
1226

1327
expect(screen.getByText('Hello Mark')).toBeTruthy();
1428
});

Diff for: apps/example-app/src/app/examples/16-input-getter-setter.spec.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,29 @@ test('should run logic in the input setter and getter', async () => {
1010
expect(getterValueControl).toHaveTextContent('I am value from getter Angular');
1111
});
1212

13-
test('should run logic in the input setter and getter while re-rendering', async () => {
14-
const { rerender } = await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
13+
test('should run logic in the input setter and getter while changing', async () => {
14+
const { change } = await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
1515
const valueControl = screen.getByTestId('value');
1616
const getterValueControl = screen.getByTestId('value-getter');
1717

1818
expect(valueControl).toHaveTextContent('I am value from setter Angular');
1919
expect(getterValueControl).toHaveTextContent('I am value from getter Angular');
2020

21-
await rerender({ value: 'React' });
21+
await change({ value: 'React' });
2222

2323
expect(valueControl).toHaveTextContent('I am value from setter React');
2424
expect(getterValueControl).toHaveTextContent('I am value from getter React');
2525
});
26+
27+
test('should run logic in the input setter and getter while re-rendering', async () => {
28+
const { rerender } = await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
29+
30+
expect(screen.getByTestId('value')).toHaveTextContent('I am value from setter Angular');
31+
expect(screen.getByTestId('value-getter')).toHaveTextContent('I am value from getter Angular');
32+
33+
await rerender({ value: 'React' });
34+
35+
// note we have to re-query because the elements are not the same anymore
36+
expect(screen.getByTestId('value')).toHaveTextContent('I am value from setter React');
37+
expect(screen.getByTestId('value-getter')).toHaveTextContent('I am value from getter React');
38+
});

Diff for: projects/testing-library/src/lib/models.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,16 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
5454
navigate: (elementOrPath: Element | string, basePath?: string) => Promise<boolean>;
5555
/**
5656
* @description
57-
* Re-render the same component with different props.
57+
* Re-render the same component with different properties.
58+
* This creates a new instance of the component.
5859
*/
59-
rerender: (componentProperties: Partial<ComponentType>) => void;
60+
rerender: (rerenderedProperties: Partial<ComponentType>) => void;
61+
62+
/**
63+
* @description
64+
* Keeps the current fixture intact and invokes ngOnChanges with the updated properties.
65+
*/
66+
change: (changedProperties: Partial<ComponentType>) => void;
6067
}
6168

6269
export interface RenderComponentOptions<ComponentType, Q extends Queries = typeof queries> {

Diff for: projects/testing-library/src/lib/testing-library.ts

+60-46
Original file line numberDiff line numberDiff line change
@@ -97,53 +97,30 @@ export async function render<SutType, WrapperType = SutType>(
9797
schemas: [...schemas],
9898
});
9999

100-
if (componentProviders) {
101-
componentProviders
102-
.reduce((acc, provider) => acc.concat(provider), [])
103-
.forEach((p) => {
104-
const { provide, ...provider } = p;
105-
TestBed.overrideProvider(provide, provider);
106-
});
107-
}
108-
109-
const fixture = await createComponentFixture(sut, { template, wrapper });
110-
setComponentProperties(fixture, { componentProperties });
111-
112-
if (removeAngularAttributes) {
113-
fixture.nativeElement.removeAttribute('ng-version');
114-
const idAttribute = fixture.nativeElement.getAttribute('id');
115-
if (idAttribute && idAttribute.startsWith('root')) {
116-
fixture.nativeElement.removeAttribute('id');
117-
}
118-
}
119-
120-
mountedFixtures.add(fixture);
121-
122100
await TestBed.compileComponents();
123101

124-
let isAlive = true;
125-
fixture.componentRef.onDestroy(() => (isAlive = false));
102+
componentProviders
103+
.reduce((acc, provider) => acc.concat(provider), [])
104+
.forEach((p) => {
105+
const { provide, ...provider } = p;
106+
TestBed.overrideProvider(provide, provider);
107+
});
126108

127-
function detectChanges() {
128-
if (isAlive) {
129-
fixture.detectChanges();
130-
}
131-
}
109+
const componentContainer = createComponentFixture(sut, { template, wrapper });
132110

133-
// Call ngOnChanges on initial render
134-
if (hasOnChangesHook(fixture.componentInstance)) {
135-
const changes = getChangesObj(null, componentProperties);
136-
fixture.componentInstance.ngOnChanges(changes);
137-
}
111+
let fixture: ComponentFixture<SutType>;
112+
let detectChanges: () => void;
138113

139-
if (detectChangesOnRender) {
140-
detectChanges();
141-
}
114+
await renderFixture(componentProperties);
115+
116+
const rerender = async (rerenderedProperties: Partial<SutType>) => {
117+
await renderFixture(rerenderedProperties);
118+
};
142119

143-
const rerender = (rerenderedProperties: Partial<SutType>) => {
144-
const changes = getChangesObj(fixture.componentInstance, rerenderedProperties);
120+
const change = (changedProperties: Partial<SutType>) => {
121+
const changes = getChangesObj(fixture.componentInstance, changedProperties);
145122

146-
setComponentProperties(fixture, { componentProperties: rerenderedProperties });
123+
setComponentProperties(fixture, { componentProperties: changedProperties });
147124

148125
if (hasOnChangesHook(fixture.componentInstance)) {
149126
fixture.componentInstance.ngOnChanges(changes);
@@ -192,9 +169,10 @@ export async function render<SutType, WrapperType = SutType>(
192169

193170
return {
194171
fixture,
195-
detectChanges,
172+
detectChanges: () => detectChanges(),
196173
navigate,
197174
rerender,
175+
change,
198176
debugElement: typeof sut === 'string' ? fixture.debugElement : fixture.debugElement.query(By.directive(sut)),
199177
container: fixture.nativeElement,
200178
debug: (element = fixture.nativeElement, maxLength, options) =>
@@ -203,6 +181,42 @@ export async function render<SutType, WrapperType = SutType>(
203181
: console.log(dtlPrettyDOM(element, maxLength, options)),
204182
...replaceFindWithFindAndDetectChanges(dtlGetQueriesForElement(fixture.nativeElement, queries)),
205183
};
184+
185+
async function renderFixture(properties: Partial<SutType>) {
186+
if (fixture) {
187+
cleanupAtFixture(fixture);
188+
}
189+
190+
fixture = await createComponent(componentContainer);
191+
setComponentProperties(fixture, { componentProperties: properties });
192+
193+
if (removeAngularAttributes) {
194+
fixture.nativeElement.removeAttribute('ng-version');
195+
const idAttribute = fixture.nativeElement.getAttribute('id');
196+
if (idAttribute && idAttribute.startsWith('root')) {
197+
fixture.nativeElement.removeAttribute('id');
198+
}
199+
}
200+
mountedFixtures.add(fixture);
201+
202+
let isAlive = true;
203+
fixture.componentRef.onDestroy(() => (isAlive = false));
204+
205+
if (hasOnChangesHook(fixture.componentInstance)) {
206+
const changes = getChangesObj(null, componentProperties);
207+
fixture.componentInstance.ngOnChanges(changes);
208+
}
209+
210+
detectChanges = () => {
211+
if (isAlive) {
212+
fixture.detectChanges();
213+
}
214+
};
215+
216+
if (detectChangesOnRender) {
217+
detectChanges();
218+
}
219+
}
206220
}
207221

208222
async function createComponent<SutType>(component: Type<SutType>): Promise<ComponentFixture<SutType>> {
@@ -211,19 +225,19 @@ async function createComponent<SutType>(component: Type<SutType>): Promise<Compo
211225
return TestBed.createComponent(component);
212226
}
213227

214-
async function createComponentFixture<SutType>(
228+
function createComponentFixture<SutType>(
215229
sut: Type<SutType> | string,
216230
{ template, wrapper }: Pick<RenderDirectiveOptions<any>, 'template' | 'wrapper'>,
217-
): Promise<ComponentFixture<SutType>> {
231+
): Type<any> {
218232
if (typeof sut === 'string') {
219233
TestBed.overrideTemplate(wrapper, sut);
220-
return createComponent(wrapper);
234+
return wrapper;
221235
}
222236
if (template) {
223237
TestBed.overrideTemplate(wrapper, template);
224-
return createComponent(wrapper);
238+
return wrapper;
225239
}
226-
return createComponent(sut);
240+
return sut;
227241
}
228242

229243
function setComponentProperties<SutType>(

Diff for: projects/testing-library/tests/change.spec.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
2+
import { render, screen } from '../src/public_api';
3+
4+
@Component({
5+
selector: 'atl-fixture',
6+
template: ` {{ firstName }} {{ lastName }} `,
7+
})
8+
class FixtureComponent {
9+
@Input() firstName = 'Sarah';
10+
@Input() lastName;
11+
}
12+
13+
test('changes the component with updated props', async () => {
14+
const { change } = await render(FixtureComponent);
15+
expect(screen.getByText('Sarah')).toBeInTheDocument();
16+
17+
const firstName = 'Mark';
18+
change({ firstName });
19+
20+
expect(screen.getByText(firstName)).toBeInTheDocument();
21+
});
22+
23+
test('changes the component with updated props while keeping other props untouched', async () => {
24+
const firstName = 'Mark';
25+
const lastName = 'Peeters';
26+
const { change } = await render(FixtureComponent, {
27+
componentProperties: {
28+
firstName,
29+
lastName,
30+
},
31+
});
32+
33+
expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
34+
35+
const firstName2 = 'Chris';
36+
change({ firstName: firstName2 });
37+
38+
expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
39+
});
40+
41+
@Component({
42+
selector: 'atl-fixture',
43+
template: ` {{ name }} `,
44+
})
45+
class FixtureWithNgOnChangesComponent implements OnChanges {
46+
@Input() name = 'Sarah';
47+
@Input() nameChanged: (name: string, isFirstChange: boolean) => void;
48+
49+
ngOnChanges(changes: SimpleChanges) {
50+
if (changes.name && this.nameChanged) {
51+
this.nameChanged(changes.name.currentValue, changes.name.isFirstChange());
52+
}
53+
}
54+
}
55+
56+
test('will call ngOnChanges on change', async () => {
57+
const nameChanged = jest.fn();
58+
const componentProperties = { nameChanged };
59+
const { change } = await render(FixtureWithNgOnChangesComponent, { componentProperties });
60+
expect(screen.getByText('Sarah')).toBeInTheDocument();
61+
62+
const name = 'Mark';
63+
change({ name });
64+
65+
expect(screen.getByText(name)).toBeInTheDocument();
66+
expect(nameChanged).toHaveBeenCalledWith(name, false);
67+
});
68+
69+
@Component({
70+
changeDetection: ChangeDetectionStrategy.OnPush,
71+
selector: 'atl-fixture',
72+
template: ` <div data-testid="number" [class.active]="activeField === 'number'">Number</div> `,
73+
})
74+
class FixtureWithOnPushComponent {
75+
@Input() activeField: string;
76+
}
77+
78+
test('update properties on change', async () => {
79+
const { change } = await render(FixtureWithOnPushComponent);
80+
const numberHtmlElementRef = screen.queryByTestId('number');
81+
82+
expect(numberHtmlElementRef).not.toHaveClass('active');
83+
change({ activeField: 'number' });
84+
expect(numberHtmlElementRef).toHaveClass('active');
85+
});

0 commit comments

Comments
 (0)