Files
2023-01-06 09:01:02 +02:00

494 lines
14 KiB
TypeScript

import {
bootstrap,
element as $,
IAugmentedJQuery,
ICompileService,
IComponentOptions, IController,
IHttpResponse,
IHttpService,
IPromise,
IScope,
module
} from 'angular'
import * as angular from 'angular'
import 'angular-mocks'
import { $http, $rootScope } from 'ngimport'
import * as PropTypes from 'prop-types'
import * as React from 'react'
import { Simulate } from 'react-dom/test-utils'
import { react2angular } from './'
class TestOne extends React.Component<Props> {
render() {
return <div>
<p>Foo: {this.props.foo}</p>
<p>Bar: {this.props.bar.join(',')}</p>
<p onClick={() => this.props.baz(42)}>Baz</p>
{this.props.children}
</div>
}
componentWillUnmount() { }
}
const TestTwo: React.FunctionComponent<Props> = props =>
<div>
<p>Foo: {props.foo}</p>
<p>Bar: {props.bar.join(',')}</p>
<p onClick={() => props.baz(42)}>Baz</p>
{props.children}
</div>
const TestThree: React.FunctionComponent = () =>
<div>Foo</div>
class TestFour extends React.Component<Props> {
render() {
return <div>Foo</div>
}
}
class TestFive extends React.Component<Props> {
static propTypes = {
bar: PropTypes.array.isRequired,
baz: PropTypes.func.isRequired,
foo: PropTypes.number.isRequired
}
render() {
return <div>
<p>Foo: {this.props.foo}</p>
<p>Bar: {this.props.bar.join(',')}</p>
<p onClick={() => this.props.baz(42)}>Baz</p>
{this.props.children}
</div>
}
componentWillUnmount() { }
}
class TestSixService {
constructor() { }
foo() {
return new Promise((resolve) => resolve('testSixService result'))
}
}
type DIProps = {
$element: IAugmentedJQuery
$http: IHttpService
testSixService: TestSixService
}
class TestSix extends React.Component<Props & DIProps> {
state = {
elementText: '',
result: '',
testSixService: ''
}
render() {
return <div>
<p>{this.state.result}</p>
<p>{this.state.elementText}</p>
<p>{this.state.testSixService}</p>
<p>{this.props.foo}</p>
<span>$element result</span>
</div>
}
componentDidMount() {
this.setState({
elementText: this.props.$element.find('span').text()
})
this.props.$http.get('https://example.com/').then(_ => {
this.setState({ result: _.data })
}
)
this.props.testSixService.foo().then(_ => {
this.setState({ testSixService: _ })
})
}
}
function TestSeven(props: Props) {
return <p>{props.foo}</p>
}
interface TestEightProps {
onChange: jasmine.Spy,
onComponentWillUnmount: jasmine.Spy,
onRender: jasmine.Spy,
values: string[],
}
class TestEight extends React.Component<TestEightProps> {
render() {
this.props.onRender()
return this.props.values
.map((value, index) => <div key={index}>{value}</div>)
}
componentWillUnmount() {
this.props.onComponentWillUnmount()
this.props.onChange(this.props.values
.map(val => `${val}ss`))
}
}
class TestEightWrapper implements IComponentOptions {
bindings = {
onComponentWillUnmount: '<',
onRender: '<',
values: '<'
}
template = `<test-angular-eight
on-change="$ctrl.onChange"
on-component-will-unmount="$ctrl.onComponentWillUnmount"
on-render="$ctrl.onRender"
values="$ctrl.values">
</test-angular-eight>`
controller = class implements IController {
values!: string[]
constructor(
private $scope: IScope
) { }
onChange = (values: string[]) => {
this.values = values
this.$scope.$apply()
}
}
}
const TestAngularOne = react2angular(TestOne, ['foo', 'bar', 'baz'])
const TestAngularTwo = react2angular(TestTwo, ['foo', 'bar', 'baz'])
const TestAngularThree = react2angular(TestThree)
const TestAngularFour = react2angular(TestFour)
const TestAngularSix = react2angular(TestSix, ['foo'], ['$http', '$element', 'testSixService', 'foo'])
const TestAngularSeven = react2angular(TestSeven, null, ['foo'])
const TestAngularEight = react2angular(TestEight, ['values', 'onComponentWillUnmount', 'onRender', 'onChange'])
// render + mount isn't sync, this is an alternative to act()
const delay = () => new Promise(resolve => setTimeout(resolve, 10))
module('test', ['bcherny/ngimport'])
.component('testAngularOne', TestAngularOne)
.component('testAngularTwo', TestAngularTwo)
.component('testAngularThree', TestAngularThree)
.component('testAngularFour', TestAngularFour)
.service('testSixService', ['$q', TestSixService])
.constant('foo', 'CONSTANT FOO')
.component('testAngularSix', TestAngularSix)
.component('testAngularSeven', TestAngularSeven)
.component('testAngularEight', TestAngularEight)
.component('testAngularEightWrapper', new TestEightWrapper())
bootstrap($(), ['test'], { strictDi: true })
interface Props {
bar: boolean[]
baz(value: number): any
foo: number
children: React.ReactNode,
}
describe('react2angular', () => {
let $compile: any
beforeEach(() => {
(angular as any).mock.module('test');
(angular as any).mock.inject(function (_$compile_: ICompileService) {
$compile = _$compile_
})
})
describe('initialization', () => {
it('should give an angular component', () => {
expect(TestAngularOne.bindings).not.toBe(undefined)
expect(TestAngularOne.controller).not.toBe(undefined)
})
it('should use the propTypes when present and no bindingNames were specified', () => {
const reactAngularComponent = react2angular(TestFive)
expect(reactAngularComponent.bindings).toEqual({
bar: '<',
baz: '<',
foo: '<'
})
})
it('should use the bindingNames when present over the propTypes', () => {
const reactAngularComponent = react2angular(TestFive, ['foo'])
expect(reactAngularComponent.bindings).toEqual({
foo: '<'
})
})
it('should have empty bindings when parameter is an empty array', () => {
const reactAngularComponent = react2angular(TestFive, [])
expect(reactAngularComponent.bindings).toEqual({})
})
it('should have empty bindings when parameter is not passed', () => {
expect(react2angular(TestThree).bindings).toEqual({})
})
it('should use the injectNames for DI', () => {
const defaultDi = (react2angular(TestThree).controller as any).slice(0, -1)
const injectedDi = (react2angular(TestThree, null, ['foo', 'bar']).controller as any).slice(0, -1)
expect(injectedDi).toEqual(defaultDi.concat(['foo', 'bar']))
})
it('should have default DI specifications if injectNames is empty', () => {
const defaultDi = (react2angular(TestThree).controller as any).slice(0, -1)
const injectedDi = (react2angular(TestThree, null, []).controller as any).slice(0, -1)
expect(injectedDi).toEqual(defaultDi)
})
})
describe('react classes', () => {
it('should render', async () => {
const scope = Object.assign($rootScope.$new(true), {
bar: [true, false],
baz: (value: number) => value + 1,
foo: 1
})
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
expect(element.find('p').length).toBe(3)
})
it('should render (even if the component takes no props)', async () => {
const scope = $rootScope.$new(true)
const element = $(`<test-angular-four></test-angular-four>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
expect(element.text()).toBe('Foo')
})
it('should update', async () => {
const scope = Object.assign($rootScope.$new(true), {
bar: [true, false],
baz: (value: number) => value + 1,
foo: 1
})
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
expect(element.find('p').eq(1).text()).toBe('Bar: true,false')
scope.$apply(() =>
scope.bar = [false, true, true]
)
await delay()
expect(element.find('p').eq(1).text()).toBe('Bar: false,true,true')
})
it('should destroy', async () => {
const scope = Object.assign($rootScope.$new(true), {
bar: [true, false],
baz: (value: number) => value + 1,
foo: 1
})
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
spyOn(TestOne.prototype, 'componentWillUnmount')
scope.$destroy()
expect(TestOne.prototype.componentWillUnmount).toHaveBeenCalled()
})
it('should take callbacks', async () => {
const baz = jasmine.createSpy('baz')
const scope = Object.assign($rootScope.$new(true), {
bar: [true, false],
baz,
foo: 1
})
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"></test-angular-one>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
Simulate.click(element.find('p').eq(2)[0])
expect(baz).toHaveBeenCalledWith(42)
})
// TODO: support children
it('should not support children', async () => {
const scope = Object.assign($rootScope.$new(true), {
bar: [true, false],
baz: (value: number) => value + 1,
foo: 1
})
const element = $(`<test-angular-one foo="foo" bar="bar" baz="baz"><span>Transcluded</span></test-angular-one>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
expect(element.find('span').length).toBe(0)
})
it('should take injections, which override props', async () => {
spyOn($http, 'get').and.returnValue(new Promise((res) => res({ data: '$http response' })) as unknown as IPromise<IHttpResponse<unknown>>)
const scope = Object.assign($rootScope.$new(true), {
foo: 'FOO'
})
const element1 = $(`<test-angular-six foo="foo"></test-angular-six>`)
$compile(element1)(scope)
const element2 = $(`<test-angular-seven foo="foo"></test-angular-seven>`)
$compile(element2)(scope)
$rootScope.$apply()
await delay()
expect($http.get).toHaveBeenCalledWith('https://example.com/')
expect(element1.find('p').eq(1).text()).toBe('$element result', '$element is injected')
expect(element1.find('p').eq(3).text()).toBe('CONSTANT FOO', 'injections should override props')
expect(element2.find('p').text()).toBe('CONSTANT FOO', 'injections should override props')
expect(element1.find('p').eq(0).text()).toBe('$http response', '$http is injected')
expect(element1.find('p').eq(2).text()).toBe('testSixService result', 'testSixService is injected')
})
})
describe('react stateless components', () => {
it('should render', async () => {
const scope = Object.assign($rootScope.$new(true), {
bar: [true, false],
baz: (value: number) => value + 1,
foo: 1
})
const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"></test-angular-two>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
expect(element.find('p').length).toBe(3)
})
it('should render (even if the component takes no props)', async () => {
const scope = $rootScope.$new(true)
const element = $(`<test-angular-three></test-angular-three>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
expect(element.text()).toBe('Foo')
})
it('should update', async () => {
const scope = Object.assign($rootScope.$new(true), {
bar: [true, false],
baz: (value: number) => value + 1,
foo: 1
})
const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"></test-angular-two>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
expect(element.find('p').eq(1).text()).toBe('Bar: true,false')
scope.$apply(() =>
scope.bar = [false, true, true]
)
await delay()
expect(element.find('p').eq(1).text()).toBe('Bar: false,true,true')
})
// TODO: figure out how to test this
xit('should destroy', () => { })
it('should take callbacks', async () => {
const baz = jasmine.createSpy('baz')
const scope = Object.assign($rootScope.$new(true), {
bar: [true, false],
baz,
foo: 1
})
const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"></test-angular-two>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
Simulate.click(element.find('p').eq(2)[0])
expect(baz).toHaveBeenCalledWith(42)
})
// TODO: support children
it('should not support children', async () => {
const scope = Object.assign($rootScope.$new(true), {
bar: [true, false],
baz: (value: number) => value + 1,
foo: 1
})
const element = $(`<test-angular-two foo="foo" bar="bar" baz="baz"><span>Transcluded</span></test-angular-two>`)
$compile(element)(scope)
$rootScope.$apply()
await delay()
expect(element.find('span').length).toBe(0)
})
it('should not call render after component unmount', async () => {
const componentWillUnmountSpy = jasmine.createSpy('componentWillUnmount')
const renderSpy = jasmine.createSpy('render')
const scope = Object.assign($rootScope.$new(true), {
onComponentWillUnmount: componentWillUnmountSpy,
onRender: renderSpy,
values: ['val1']
})
const element = $(`
<test-angular-eight-wrapper
on-render="onRender"
on-component-will-unmount="onComponentWillUnmount"
values="values">
</test-angular-eight-wrapper>
`)
$compile(element)(scope)
const childScope = angular
.element(element.find('test-angular-eight'))
.scope()
$rootScope.$apply()
await delay()
// Erase first render caused on apply
renderSpy.calls.reset()
// Destroy child component to cause unmount
childScope.$destroy()
// Make sure render on child was not called after unmount
expect(componentWillUnmountSpy.calls.count()).toEqual(1)
expect(renderSpy.calls.count()).toEqual(0)
expect(componentWillUnmountSpy).not.toHaveBeenCalledBefore(renderSpy)
})
})
})