From d5486f1ec61bf3a045cf61167c03a85634e963a8 Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Sat, 2 Dec 2023 19:34:31 +0100
Subject: [PATCH] feat: add partialUpdate to rerender (#427)

Closes #411
---
 projects/testing-library/src/lib/models.ts    |  2 +-
 .../src/lib/testing-library.ts                | 21 +++++--
 .../testing-library/tests/rerender.spec.ts    | 58 +++++++++++++++++++
 3 files changed, 76 insertions(+), 5 deletions(-)

diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts
index f148ee2..257bef0 100644
--- a/projects/testing-library/src/lib/models.ts
+++ b/projects/testing-library/src/lib/models.ts
@@ -61,7 +61,7 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
     properties?: Pick<
       RenderTemplateOptions<ComponentType>,
       'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender'
-    >,
+    > & { partialUpdate?: boolean },
   ) => Promise<void>;
   /**
    * @description
diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts
index 48128cc..1be2f22 100644
--- a/projects/testing-library/src/lib/testing-library.ts
+++ b/projects/testing-library/src/lib/testing-library.ts
@@ -182,10 +182,16 @@ export async function render<SutType, WrapperType = SutType>(
     properties?: Pick<
       RenderTemplateOptions<SutType>,
       'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender'
-    >,
+    > & { partialUpdate?: boolean },
   ) => {
     const newComponentInputs = properties?.componentInputs ?? {};
-    const changesInComponentInput = update(fixture, renderedInputKeys, newComponentInputs, setComponentInputs);
+    const changesInComponentInput = update(
+      fixture,
+      renderedInputKeys,
+      newComponentInputs,
+      setComponentInputs,
+      properties?.partialUpdate ?? false,
+    );
     renderedInputKeys = Object.keys(newComponentInputs);
 
     const newComponentOutputs = properties?.componentOutputs ?? {};
@@ -198,7 +204,13 @@ export async function render<SutType, WrapperType = SutType>(
     renderedOutputKeys = Object.keys(newComponentOutputs);
 
     const newComponentProps = properties?.componentProperties ?? {};
-    const changesInComponentProps = update(fixture, renderedPropKeys, newComponentProps, setComponentProperties);
+    const changesInComponentProps = update(
+      fixture,
+      renderedPropKeys,
+      newComponentProps,
+      setComponentProperties,
+      properties?.partialUpdate ?? false,
+    );
     renderedPropKeys = Object.keys(newComponentProps);
 
     if (hasOnChangesHook(fixture.componentInstance)) {
@@ -387,12 +399,13 @@ function update<SutType>(
     fixture: ComponentFixture<SutType>,
     values: RenderTemplateOptions<SutType>['componentInputs' | 'componentProperties'],
   ) => void,
+  partialUpdate: boolean,
 ) {
   const componentInstance = fixture.componentInstance as Record<string, any>;
   const simpleChanges: SimpleChanges = {};
 
   for (const key of prevRenderedKeys) {
-    if (!Object.prototype.hasOwnProperty.call(newValues, key)) {
+    if (!partialUpdate && !Object.prototype.hasOwnProperty.call(newValues, key)) {
       simpleChanges[key] = new SimpleChange(componentInstance[key], undefined, false);
       delete componentInstance[key];
     }
diff --git a/projects/testing-library/tests/rerender.spec.ts b/projects/testing-library/tests/rerender.spec.ts
index 9c25257..571d642 100644
--- a/projects/testing-library/tests/rerender.spec.ts
+++ b/projects/testing-library/tests/rerender.spec.ts
@@ -83,6 +83,35 @@ test('rerenders the component with updated inputs and resets other props', async
   });
 });
 
+test('rerenders the component with updated inputs and keeps other props when partial is true', async () => {
+  const firstName = 'Mark';
+  const lastName = 'Peeters';
+  const { rerender } = await render(FixtureComponent, {
+    componentInputs: {
+      firstName,
+      lastName,
+    },
+  });
+
+  expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
+
+  const firstName2 = 'Chris';
+  await rerender({ componentInputs: { firstName: firstName2 }, partialUpdate: true });
+
+  expect(screen.queryByText(firstName)).not.toBeInTheDocument();
+  expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
+
+  expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
+  const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
+  expect(rerenderedChanges).toEqual({
+    firstName: {
+      previousValue: 'Mark',
+      currentValue: 'Chris',
+      firstChange: false,
+    },
+  });
+});
+
 test('rerenders the component with updated props and resets other props with componentProperties', async () => {
   const firstName = 'Mark';
   const lastName = 'Peeters';
@@ -118,6 +147,35 @@ test('rerenders the component with updated props and resets other props with com
   });
 });
 
+test('rerenders the component with updated props keeps other props when partial is true', async () => {
+  const firstName = 'Mark';
+  const lastName = 'Peeters';
+  const { rerender } = await render(FixtureComponent, {
+    componentProperties: {
+      firstName,
+      lastName,
+    },
+  });
+
+  expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
+
+  const firstName2 = 'Chris';
+  await rerender({ componentProperties: { firstName: firstName2 }, partialUpdate: true });
+
+  expect(screen.queryByText(firstName)).not.toBeInTheDocument();
+  expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
+
+  expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
+  const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
+  expect(rerenderedChanges).toEqual({
+    firstName: {
+      previousValue: 'Mark',
+      currentValue: 'Chris',
+      firstChange: false,
+    },
+  });
+});
+
 test('change detection gets not called if `detectChangesOnRender` is set to false', async () => {
   const { rerender } = await render(FixtureComponent);
   expect(screen.getByText('Sarah')).toBeInTheDocument();