More tests

This commit is contained in:
Josep M Sobrepere 2020-06-22 00:19:52 +02:00
parent b6c66c6597
commit 8c56b0a80b
8 changed files with 479 additions and 170 deletions

159
package-lock.json generated
View File

@ -2464,6 +2464,159 @@
"micromatch": "^4.0.2"
}
},
"@testing-library/dom": {
"version": "7.16.2",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.16.2.tgz",
"integrity": "sha512-4fT5l5L+5gfNhUZVCg0wnSszbRJ7A1ZHEz32v7OzH3mcY5lUsK++brI3IB2L9F5zO4kSDc2TRGEVa8v2hgl9vA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.10.2",
"aria-query": "^4.0.2",
"dom-accessibility-api": "^0.4.5",
"pretty-format": "^25.5.0"
},
"dependencies": {
"@babel/runtime": {
"version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
"integrity": "sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"@babel/runtime-corejs3": {
"version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz",
"integrity": "sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw==",
"dev": true,
"requires": {
"core-js-pure": "^3.0.0",
"regenerator-runtime": "^0.13.4"
}
},
"@jest/types": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz",
"integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==",
"dev": true,
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^1.1.1",
"@types/yargs": "^15.0.0",
"chalk": "^3.0.0"
}
},
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
"integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"aria-query": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
"integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.10.2",
"@babel/runtime-corejs3": "^7.10.2"
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"pretty-format": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz",
"integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==",
"dev": true,
"requires": {
"@jest/types": "^25.5.0",
"ansi-regex": "^5.0.0",
"ansi-styles": "^4.0.0",
"react-is": "^16.12.0"
}
},
"supports-color": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"@testing-library/react": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.3.0.tgz",
"integrity": "sha512-Rhn5uJK6lYHWzlGVbK6uAvheAW8AUoFYxTLGdDxgsJDaK/PYy5drWfW/6YpMMOKMw+u6jHHl4MNHlt5qLHnm0Q==",
"dev": true,
"requires": {
"@babel/runtime": "^7.10.2",
"@testing-library/dom": "^7.14.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
"integrity": "sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
}
}
},
"@testing-library/react-hooks": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-3.3.0.tgz",
@ -4509,6 +4662,12 @@
"esutils": "^2.0.2"
}
},
"dom-accessibility-api": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.5.tgz",
"integrity": "sha512-HcPDilI95nKztbVikaN2vzwvmv0sE8Y2ZJFODy/m15n7mGXLeOKGiys9qWVbFbh+aq/KYj2lqMLybBOkYAEXqg==",
"dev": true
},
"domexception": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",

View File

@ -37,6 +37,7 @@
"module": "dist/re-rxjs.esm.js",
"devDependencies": {
"@josepot/rxjs-utils": "^0.12.0",
"@testing-library/react": "^10.3.0",
"@testing-library/react-hooks": "^3.3.0",
"@types/jest": "^26.0.0",
"@types/react": "^16.9.38",

View File

@ -26,9 +26,6 @@ const reactEnhancer = <T>(
error(e) {
subscriber.error(e)
},
complete() {
subscriber.complete()
},
})
onSubscribe.next()
finalizeLastUnsubscription()

View File

@ -1,71 +1,129 @@
import { connectFactoryObservable } from "../src"
import { connectFactoryObservable } from "../src/connectFactoryObservable"
import { from, of, defer, concat, BehaviorSubject } from "rxjs"
import { renderHook, act } from "@testing-library/react-hooks"
const wait = (ms: number) => new Promise(res => setTimeout(res, ms))
describe("connectObservable", () => {
it("returns the latest emitted value", async () => {
const [useNumber] = connectFactoryObservable((id: number) => of(id))
const { result } = renderHook(() => useNumber(1))
expect(result.current).toBe(1)
describe("connectFactoryObservable", () => {
const originalError = console.error
beforeAll(() => {
console.error = (...args: any) => {
if (/Consider adding an error/.test(args[0])) {
return
}
originalError.call(console, ...args)
}
})
it("shares the source subscription until the refCount has stayed at zero for the grace-period", async () => {
let nInitCount = 0
const observable$ = defer(() => {
nInitCount += 1
return from([1, 2, 3, 4, 5])
afterAll(() => {
console.error = originalError
})
describe("hook", () => {
it("returns the latest emitted value", async () => {
const [useNumber] = connectFactoryObservable((id: number) => of(id))
const { result } = renderHook(() => useNumber(1))
expect(result.current).toBe(1)
})
const [useLatestNumber] = connectFactoryObservable(
(id: number) => concat(observable$, of(id)),
{
unsubscribeGraceTime: 100,
},
)
const { unmount } = renderHook(() => useLatestNumber(6))
const { unmount: unmount2 } = renderHook(() => useLatestNumber(6))
const { unmount: unmount3 } = renderHook(() => useLatestNumber(6))
expect(nInitCount).toBe(1)
unmount()
unmount2()
unmount3()
it("shares the source subscription until the refCount has stayed at zero for the grace-period", async () => {
let nInitCount = 0
const observable$ = defer(() => {
nInitCount += 1
return from([1, 2, 3, 4, 5])
})
await wait(90)
const { unmount: unmount4 } = renderHook(() => useLatestNumber(6))
expect(nInitCount).toBe(1)
unmount4()
const [useLatestNumber] = connectFactoryObservable(
(id: number) => concat(observable$, of(id)),
{
unsubscribeGraceTime: 100,
},
)
const { unmount } = renderHook(() => useLatestNumber(6))
const { unmount: unmount2 } = renderHook(() => useLatestNumber(6))
const { unmount: unmount3 } = renderHook(() => useLatestNumber(6))
expect(nInitCount).toBe(1)
unmount()
unmount2()
unmount3()
await wait(110)
renderHook(() => useLatestNumber(6))
expect(nInitCount).toBe(2)
await wait(90)
const { unmount: unmount4 } = renderHook(() => useLatestNumber(6))
expect(nInitCount).toBe(1)
unmount4()
await wait(110)
renderHook(() => useLatestNumber(6))
expect(nInitCount).toBe(2)
})
it("allows errors to be caught in error boundaries", () => {
const errStream = new BehaviorSubject(1)
const [useError] = connectFactoryObservable(() => errStream)
renderHook(() => useError())
expect(() =>
act(() => {
errStream.error("error")
}),
).toThrow()
})
it("doesn't throw errors on components that will get unmounted on the next cycle", () => {
const errStream = new BehaviorSubject(1)
const [useError] = connectFactoryObservable(() => errStream)
const { unmount } = renderHook(() => useError())
expect(() =>
act(() => {
errStream.error("error")
unmount()
}),
).not.toThrow()
})
})
describe("observable", () => {
it("returns a factory of BehaviorObservables", () => {
const [, getShared] = connectFactoryObservable((x: number) => of(x))
expect((getShared(0) as any).getValue).toBeInstanceOf(Function)
})
it("allows errors to be caught in error boundaries", () => {
const errStream = new BehaviorSubject(1)
const [useError] = connectFactoryObservable(() => errStream)
it("if the source observable completes it keeps emitting the latest value until there are no more subscriptions", () => {
let diff = -1
const [, getShared] = connectFactoryObservable((_: number) => {
diff++
return from([1, 2, 3, 4].map(val => val + diff))
})
renderHook(() => useError())
let latestValue1: number = 0
let nUpdates = 0
const sub1 = getShared(0).subscribe(x => {
latestValue1 = x
nUpdates += 1
})
expect(latestValue1).toBe(4)
expect(nUpdates).toBe(1)
expect(() =>
act(() => {
errStream.error("error")
}),
).toThrow()
})
let latestValue2: number = 0
const sub2 = getShared(0).subscribe(x => {
latestValue2 = x
nUpdates += 1
})
expect(latestValue2).toBe(4)
expect(nUpdates).toBe(2)
it("doesn't throw errors on components that will get unmounted on the next cycle", () => {
const errStream = new BehaviorSubject(1)
const [useError] = connectFactoryObservable(() => errStream)
sub1.unsubscribe()
sub2.unsubscribe()
const { unmount } = renderHook(() => useError())
expect(() =>
act(() => {
errStream.error("error")
unmount()
}),
).not.toThrow()
let latestValue3: number = 0
const sub3 = getShared(0).subscribe(x => {
latestValue3 = x
nUpdates += 1
})
expect(latestValue3).toBe(5)
expect(nUpdates).toBe(3)
sub3.unsubscribe()
})
})
})

View File

@ -1,10 +1,29 @@
import { connectObservable } from "../src"
import { from, of, defer } from "rxjs"
import React, { Suspense } from "react"
import { render, fireEvent, screen } from "@testing-library/react"
import { connectObservable } from "../src/connectObservable"
import { switchMapSuspended } from "../src/operators/switchMapSuspended"
import { suspended } from "../src/operators/suspended"
import { from, of, defer, Subject, concat } from "rxjs"
import { renderHook } from "@testing-library/react-hooks"
import { delay, scan, take, mergeMap, mergeMapTo } from "rxjs/operators"
const wait = (ms: number) => new Promise(res => setTimeout(res, ms))
describe("connectObservable", () => {
const originalError = console.error
beforeAll(() => {
console.error = (...args: any) => {
if (/Warning.*not wrapped in act/.test(args[0])) {
return
}
originalError.call(console, ...args)
}
})
afterAll(() => {
console.error = originalError
})
it("sets the initial state synchronously if it's available", async () => {
const observable$ = of(1)
const [useLatestNumber] = connectObservable(observable$)
@ -40,4 +59,63 @@ describe("connectObservable", () => {
renderHook(() => useLatestNumber())
expect(nInitCount).toBe(2)
})
it("works with suspense", async () => {
const subject$ = new Subject()
const source$ = concat(
subject$.pipe(
take(2),
scan(a => a + 1, 0),
switchMapSuspended(x => of(x).pipe(delay(100))),
),
subject$.pipe(take(1), mergeMapTo(of(3).pipe(delay(100), suspended()))),
)
const [useDelayedNumber] = connectObservable(source$)
const Result: React.FC = () => <div>Result {useDelayedNumber()}</div>
const TestSuspense: React.FC = () => {
return (
<div>
<button onClick={() => subject$.next()}>Next</button>
<Suspense fallback={<span>Waiting</span>}>
<Result />
</Suspense>
</div>
)
}
render(<TestSuspense />)
expect(screen.queryByText("Result")).toBeNull()
expect(screen.queryByText("Waiting")).not.toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result")).toBeNull()
expect(screen.queryByText("Waiting")).not.toBeNull()
await wait(110)
expect(screen.queryByText("Result 1")).not.toBeNull()
expect(screen.queryByText("Waiting")).toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result")).toBeNull()
expect(screen.queryByText("Waiting")).not.toBeNull()
await wait(110)
expect(screen.queryByText("Result 2")).not.toBeNull()
expect(screen.queryByText("Waiting")).toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result")).toBeNull()
expect(screen.queryByText("Waiting")).not.toBeNull()
await wait(110)
expect(screen.queryByText("Result 3")).not.toBeNull()
expect(screen.queryByText("Waiting")).toBeNull()
})
})

View File

@ -1,4 +1,4 @@
import { createInput } from "../"
import { createInput } from "../src/createInput"
describe("createInput", () => {
test("it returns the default value", () => {

View File

@ -1,112 +0,0 @@
import {} from "react/experimental"
import { defer, of, Subject, Observable, NEVER, concat } from "rxjs"
import { SUSPENSE } from "../src"
import { useObservable } from "../src/useObservable"
import { renderHook, act } from "@testing-library/react-hooks"
import { delay, switchMap, startWith } from "rxjs/operators"
import { unstable_useTransition as useTransition, useEffect } from "react"
import { BehaviorObservable, distinctShareReplay } from "../src"
import reactEnhancer from "../src/operators/react-enhancer"
const wait = (ms: number) => new Promise(res => setTimeout(res, ms))
const enhancer = <T>(source$: Observable<T>) =>
reactEnhancer(concat(source$, NEVER).pipe(distinctShareReplay()), 20)
describe("useObservable", () => {
it("works", async () => {
let counter = 0
let subject = new Subject<number>()
const source$ = defer(() => {
subject = new Subject<number>()
counter++
return subject.asObservable()
}).pipe(enhancer) as BehaviorObservable<number>
const { result, unmount } = renderHook(() => useObservable(source$))
expect(result.current).toBe(null)
await act(async () => {
subject.next(1)
await wait(10)
})
expect(counter).toBe(1)
expect(result.current).toEqual(1)
act(() => {
subject.next(4)
subject.complete()
})
expect(result.current).toEqual(4)
expect(counter).toBe(1)
unmount()
const secondMount = renderHook(() => useObservable(source$))
expect(counter).toBe(1)
expect(secondMount.result.current).toEqual(4)
secondMount.unmount()
await wait(40)
const thirdMount = renderHook(() => useObservable(source$))
expect(thirdMount.result.current).toEqual(null)
expect(counter).toBe(2)
await act(async () => {
subject.next(1)
await wait(10)
})
expect(thirdMount.result.current).toEqual(1)
expect(counter).toBe(2)
})
const userId$ = new Subject<number>()
const getUser = (id: number) =>
of({ id, name: `name_${id}` }).pipe(delay(200))
const user$ = userId$
.pipe(
switchMap(id => getUser(id).pipe(startWith(SUSPENSE))),
startWith({ id: 1, name: "name_1" }),
)
.pipe(enhancer) as BehaviorObservable<any>
const useUserTransition = () => {
const [startTransition, isPending] = useTransition({ timeoutMs: 500 })
useEffect(() => {
setTimeout(() => {
startTransition(() => {
userId$.next(8)
})
}, 100)
}, [])
// console.log("isPending", isPending)
return isPending
}
it.skip("works with useTransition", async () => {
const { result: userResult } = renderHook(() => useObservable(user$))
const { result: transitionResult } = renderHook(() => useUserTransition())
expect(userResult.current).toEqual({ id: 1, name: "name_1" })
expect(transitionResult.current).toBe(false)
await act(async () => {
await wait(120)
})
expect(userResult.current).toEqual({ id: 1, name: "name_1" })
expect(transitionResult.current).toBe(true)
await act(async () => {
await wait(220)
})
expect(userResult.current).toEqual({ id: 8, name: "name_8" })
expect(transitionResult.current).toBe(false)
})
})

128
test/useObservable.test.tsx Normal file
View File

@ -0,0 +1,128 @@
import React, { useState, Suspense } from "react"
import { render, fireEvent, screen } from "@testing-library/react"
import { defer, of, Subject, NEVER, concat } from "rxjs"
import { useObservable } from "../src/useObservable"
import { renderHook, act } from "@testing-library/react-hooks"
import { BehaviorObservable, distinctShareReplay } from "../src"
import reactEnhancer from "../src/operators/react-enhancer"
import { SUSPENSE } from "../src/SUSPENSE"
const wait = (ms: number) => new Promise(res => setTimeout(res, ms))
const enhancer = (source$: any) =>
reactEnhancer(concat(source$, NEVER).pipe(distinctShareReplay()), 20)
describe("useObservable", () => {
it("works", async () => {
let counter = 0
let subject = new Subject<number>()
const source$ = defer(() => {
subject = new Subject<number>()
counter++
return subject.asObservable()
}).pipe(enhancer) as BehaviorObservable<number>
const { result, unmount } = renderHook(() => useObservable(source$))
expect(result.current).toBe(null)
await act(async () => {
subject.next(1)
await wait(10)
})
expect(counter).toBe(1)
expect(result.current).toEqual(1)
act(() => {
subject.next(4)
subject.complete()
})
expect(result.current).toEqual(4)
expect(counter).toBe(1)
unmount()
const secondMount = renderHook(() => useObservable(source$))
expect(counter).toBe(1)
expect(secondMount.result.current).toEqual(4)
secondMount.unmount()
await wait(40)
const thirdMount = renderHook(() => useObservable(source$))
expect(thirdMount.result.current).toEqual(null)
expect(counter).toBe(2)
await act(async () => {
subject.next(1)
await wait(10)
})
expect(thirdMount.result.current).toEqual(1)
expect(counter).toBe(2)
})
const observables: any = [NEVER, of(10), of(SUSPENSE), of(20)].map(enhancer)
const Result: React.FC<{ idx: number }> = ({ idx }) => {
const result = useObservable(observables[idx % observables.length])
return <div>Result {result}</div>
}
const TestSuspense: React.FC = () => {
const [currentIdx, setCurrentIdx] = useState(0)
return (
<div>
<button onClick={() => setCurrentIdx(x => x + 1)}>Next</button>
<Suspense fallback={<span>Waiting</span>}>
<Result idx={currentIdx} />
</Suspense>
</div>
)
}
it("supports suspense", () => {
const subs = observables[2].subscribe()
render(<TestSuspense />)
expect(screen.queryByText("Result")).toBeNull()
expect(screen.queryByText("Waiting")).not.toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result 10")).not.toBeNull()
expect(screen.queryByText("Waiting")).toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result")).toBeNull()
expect(screen.queryByText("Waiting")).not.toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result 20")).not.toBeNull()
expect(screen.queryByText("Waiting")).toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result")).toBeNull()
expect(screen.queryByText("Waiting")).not.toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result 10")).not.toBeNull()
expect(screen.queryByText("Waiting")).toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result")).toBeNull()
expect(screen.queryByText("Waiting")).not.toBeNull()
fireEvent.click(screen.getByText(/Next/i))
expect(screen.queryByText("Result 20")).not.toBeNull()
expect(screen.queryByText("Waiting")).toBeNull()
subs.unsubscribe()
})
})