From 81c2de90c621df4f0702db02adf0c1f31bc08cd6 Mon Sep 17 00:00:00 2001
From: Sebastian Silbermann <silbermann.sebastian@gmail.com>
Date: Tue, 2 Feb 2021 10:30:49 +0100
Subject: [PATCH] fix(render): Default to HTMLElement in returned container
 (#868)

---
 types/index.d.ts | 26 ++++++++++++++++++--------
 types/test.tsx   | 36 ++++++++++++++++++++++++++++++++----
 2 files changed, 50 insertions(+), 12 deletions(-)

diff --git a/types/index.d.ts b/types/index.d.ts
index 8e0d1c02..4d599256 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -6,12 +6,16 @@ import {
   BoundFunction,
   prettyFormat,
 } from '@testing-library/dom'
+import {Renderer} from 'react-dom'
 import {act as reactAct} from 'react-dom/test-utils'
 
 export * from '@testing-library/dom'
 
-export type RenderResult<Q extends Queries = typeof queries> = {
-  container: Element
+export type RenderResult<
+  Q extends Queries = typeof queries,
+  Container extends Element | DocumentFragment = HTMLElement
+> = {
+  container: Container
   baseElement: Element
   debug: (
     baseElement?:
@@ -26,8 +30,11 @@ export type RenderResult<Q extends Queries = typeof queries> = {
   asFragment: () => DocumentFragment
 } & {[P in keyof Q]: BoundFunction<Q[P]>}
 
-export interface RenderOptions<Q extends Queries = typeof queries> {
-  container?: Element
+export interface RenderOptions<
+  Q extends Queries = typeof queries,
+  Container extends Element | DocumentFragment = HTMLElement
+> {
+  container?: Container
   baseElement?: Element
   hydrate?: boolean
   queries?: Q
@@ -39,14 +46,17 @@ type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
 /**
  * Render into a container which is appended to document.body. It should be used with cleanup.
  */
+export function render<
+  Q extends Queries,
+  Container extends Element | DocumentFragment = HTMLElement
+>(
+  ui: React.ReactElement,
+  options: RenderOptions<Q, Container>,
+): RenderResult<Q, Container>
 export function render(
   ui: React.ReactElement,
   options?: Omit<RenderOptions, 'queries'>,
 ): RenderResult
-export function render<Q extends Queries>(
-  ui: React.ReactElement,
-  options: RenderOptions<Q>,
-): RenderResult<Q>
 
 /**
  * Unmounts React trees that were mounted with render.
diff --git a/types/test.tsx b/types/test.tsx
index 1366caac..7cc0e015 100644
--- a/types/test.tsx
+++ b/types/test.tsx
@@ -3,7 +3,7 @@ import {render, fireEvent, screen, waitFor} from '.'
 import * as pure from './pure'
 
 export async function testRender() {
-  const page = render(<div />)
+  const page = render(<button />)
 
   // single queries
   page.getByText('foo')
@@ -17,11 +17,12 @@ export async function testRender() {
 
   // helpers
   const {container, rerender, debug} = page
+  expectType<HTMLElement, typeof container>(container)
   return {container, rerender, debug}
 }
 
 export async function testPureRender() {
-  const page = pure.render(<div />)
+  const page = pure.render(<button />)
 
   // single queries
   page.getByText('foo')
@@ -35,13 +36,15 @@ export async function testPureRender() {
 
   // helpers
   const {container, rerender, debug} = page
+  expectType<HTMLElement, typeof container>(container)
   return {container, rerender, debug}
 }
 
 export function testRenderOptions() {
   const container = document.createElement('div')
   const options = {container}
-  render(<div />, options)
+  const {container: returnedContainer} = render(<button />, options)
+  expectType<HTMLDivElement, typeof returnedContainer>(returnedContainer)
 }
 
 export function testSVGRenderOptions() {
@@ -50,7 +53,8 @@ export function testSVGRenderOptions() {
     'svg',
   )
   const options = {container}
-  render(<svg />, options)
+  const {container: returnedContainer} = render(<path />, options)
+  expectType<SVGSVGElement, typeof returnedContainer>(returnedContainer)
 }
 
 export function testFireEvent() {
@@ -87,3 +91,27 @@ eslint
   testing-library/no-debug: "off",
   testing-library/prefer-screen-queries: "off"
 */
+
+// https://stackoverflow.com/questions/53807517/how-to-test-if-two-types-are-exactly-the-same
+type IfEquals<T, U, Yes = unknown, No = never> = (<G>() => G extends T
+  ? 1
+  : 2) extends <G>() => G extends U ? 1 : 2
+  ? Yes
+  : No
+
+/**
+ * Issues a type error if `Expected` is not identical to `Actual`.
+ *
+ * `Expected` should be declared when invoking `expectType`.
+ * `Actual` should almost always we be a `typeof value` statement.
+ *
+ * Source: https://github.com/mui-org/material-ui/blob/6221876a4b468a3330ffaafa8472de7613933b87/packages/material-ui-types/index.d.ts#L73-L84
+ *
+ * @example `expectType<number | string, typeof value>(value)`
+ * TypeScript issues a type error since `value is not assignable to never`.
+ * This means `typeof value` is not identical to `number | string`
+ * @param actual
+ */
+declare function expectType<Expected, Actual>(
+  actual: IfEquals<Actual, Expected, Actual>,
+): void