Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit cd17373

Browse files
feat: add rerender method (testing-library#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 2338f85 commit cd17373

File tree

6 files changed

+208
-101
lines changed

6 files changed

+208
-101
lines changed
Lines changed: 16 additions & 2 deletions
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
});

‎apps/example-app/src/app/examples/16-input-getter-setter.spec.ts

Lines changed: 16 additions & 3 deletions
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+
});

‎projects/testing-library/src/lib/models.ts

Lines changed: 9 additions & 2 deletions
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> {

‎projects/testing-library/src/lib/testing-library.ts

Lines changed: 60 additions & 46 deletions
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-
asyncfunction 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>(
Lines changed: 85 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /