我是前端测试的新手,并且对React生态系统非常了解,所以请多多包涵。
我已经对CustomerSection
组件进行了一些单元测试。运行yarn spec
时,出现以下错误:
➜ client git:(ConvertToRO_Test) ✗ yarn spec
yarn run v1.3.2
warning package.json: No license field
warning ../package.json: No license field
$ NODE_ENV=test mocha './spec/**/*.spec.js*' --compilers js:babel-register --recursive
Warning: Failed prop type: The prop `submitting` is marked as required in `Form`, but its value is `undefined`.
in Form
Warning: Failed prop type: The prop `role` is marked as required in `Form`, but its value is `undefined`.
in Form
Warning: Failed prop type: The prop `search` is marked as required in `Form`, but its value is `undefined`.
in Form
Warning: Failed prop type: The prop `manager` is marked as required in `Form`, but its value is `undefined`.
in Form
Warning: Failed prop type: The prop `id` is marked as required in `Form`, but its value is `undefined`.
in Form
Warning: Failed prop type: The prop `close` is marked as required in `Form`, but its value is `undefined`.
in Form
Warning: Failed prop type: The prop `handleSubmit` is marked as required in `Form`, but its value is `undefined`.
in Form
Warning: Failed prop type: Invalid prop `fields` of type `array` supplied to `LineItemFields`, expected `object`.
in LineItemFields
Warning: Failed prop type: The prop `company.name` is marked as required in `CompanyStatsTooltip`, but its value is `undefined`.
in CompanyStatsTooltip (created by CustomerSection)
in CustomerSection (created by ConnectedFields)
in ConnectedFields (created by Connect(ConnectedFields))
in Connect(ConnectedFields) (created by Fields)
in Fields (created by WithFormFields)
in WithFormFields (created by Form(WithFormFields))
in Form(WithFormFields) (created by Connect(Form(WithFormFields)))
in Connect(Form(WithFormFields)) (created by ReduxForm)
in ReduxForm
in Provider (created by WrapperComponent)
in WrapperComponent
The above error occurred in the <CustomerSection> component:
in CustomerSection (created by ConnectedFields)
in ConnectedFields (created by Connect(ConnectedFields))
in Connect(ConnectedFields) (created by Fields)
in Fields (created by WithFormFields)
in WithFormFields (created by Form(WithFormFields))
in Form(WithFormFields) (created by Connect(Form(WithFormFields)))
in Connect(Form(WithFormFields)) (created by ReduxForm)
in ReduxForm
in Provider (created by WrapperComponent)
in WrapperComponent
Consider adding an error boundary to your tree to customize error handling behavior.
/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20106
throw error;
^
TypeError: Cannot read property 'label' of undefined
at /Users/danielbonnell/Development/acme-api/client/app/lib/forms.js:3:30
at Array.map (<anonymous>)
at toOptions (/Users/danielbonnell/Development/acme-api/client/app/lib/forms.js:2:27)
at CustomerSection.render (/Users/danielbonnell/Development/acme-api/client/app/modules/SalesOrders/components/shared/FormSections/CustomerSection/CustomerSection.jsx:149:24)
at finishClassComponent (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:14535:31)
at updateClassComponent (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:14490:24)
at beginWork (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:15438:16)
at performUnitOfWork (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:19106:12)
at workLoop (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:19146:24)
at renderRoot (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:19229:7)
at performWorkOnRoot (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20136:7)
at performWork (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20048:7)
at performSyncWork (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20022:3)
at requestWork (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:19891:5)
at scheduleWork (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:19705:5)
at scheduleRootUpdate (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20366:3)
at updateContainerAtExpirationTime (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20394:10)
at updateContainer (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20451:10)
at ReactRoot.render (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20747:3)
at /Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20884:14
at unbatchedUpdates (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20253:10)
at legacyRenderSubtreeIntoContainer (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20880:5)
at Object.render (/Users/danielbonnell/Development/acme-api/client/node_modules/react-dom/cjs/react-dom.development.js:20949:12)
at Object.render (/Users/danielbonnell/Development/acme-api/client/node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:352:114)
at new ReactWrapper (/Users/danielbonnell/Development/acme-api/client/node_modules/enzyme/build/ReactWrapper.js:98:16)
at mount (/Users/danielbonnell/Development/acme-api/client/node_modules/enzyme/build/mount.js:19:10)
at Suite.<anonymous> (/Users/danielbonnell/Development/acme-api/client/spec/modules/SalesOrders/components/shared/FormSections/CustomerSection/CustomerSection.spec.jsx:26:19)
at Object.create (/Users/danielbonnell/Development/acme-api/client/node_modules/mocha/lib/interfaces/common.js:114:19)
at context.describe.context.context (/Users/danielbonnell/Development/acme-api/client/node_modules/mocha/lib/interfaces/bdd.js:44:27)
at Object.<anonymous> (/Users/danielbonnell/Development/acme-api/client/spec/modules/SalesOrders/components/shared/FormSections/CustomerSection/CustomerSection.spec.jsx:10:1)
at Module._compile (module.js:652:30)
at loader (/Users/danielbonnell/Development/acme-api/client/node_modules/babel-register/lib/node.js:144:5)
at Object.require.extensions.(anonymous function) [as .jsx] (/Users/danielbonnell/Development/acme-api/client/node_modules/babel-register/lib/node.js:154:7)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
at Function.Module._load (module.js:497:3)
at Module.require (module.js:596:17)
at require (internal/module.js:11:18)
at /Users/danielbonnell/Development/acme-api/client/node_modules/mocha/lib/mocha.js:222:27
at Array.forEach (<anonymous>)
at Mocha.loadFiles (/Users/danielbonnell/Development/acme-api/client/node_modules/mocha/lib/mocha.js:219:14)
at Mocha.run (/Users/danielbonnell/Development/acme-api/client/node_modules/mocha/lib/mocha.js:487:10)
at Object.<anonymous> (/Users/danielbonnell/Development/acme-api/client/node_modules/mocha/bin/_mocha:459:18)
at Module._compile (module.js:652:30)
at Object.Module._extensions..js (module.js:663:10)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
at Function.Module._load (module.js:497:3)
at Function.Module.runMain (module.js:693:10)
at startup (bootstrap_node.js:191:16)
at bootstrap_node.js:612:3
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
如果我使用shallow
渲染,则测试将运行,但由于未渲染子组件而失败。我不确定是什么问题。
这是我的CustomerSection
组件:
import React, { Component } from "react";
import T from "prop-types";
import { Row, Col } from "react-bootstrap";
import { Field } from "redux-form";
import api from "APP_ROOT/api";
import pojo from "LIB/pojos";
import { toOptions } from "LIB/forms";
import {
Select,
FormInput,
CheckGroup,
Check
} from "MODULES/shared/components/Forms";
import { CompanyStatsTooltip } from "MODULES/shared/components";
import AutoCompleteField from "MODULES/shared/components/Forms/AutoCompleteField/AutoCompleteField";
import {
WIRE_IN_ADVANCE,
CHECK_IN_ADVANCE,
CREDIT_CARD,
VENDOR_TERMS
} from "MODULES/companies/constants";
import * as F from "MODULES/shared/components/Forms/FormTypes";
import withFormFields from "MODULES/shared/decorators/withFormFields";
const baseOptions = [WIRE_IN_ADVANCE, CHECK_IN_ADVANCE, CREDIT_CARD];
class CustomerSection extends Component {
state = {
contacts: [],
terms: baseOptions,
selectedCompany: null,
afterHours: this.props.afterHours
};
componentWillMount() {
const { company, creditTerms } = this.props;
if (company) {
this.setState({ selectedCompany: company });
this.searchContacts(company.id);
}
if (creditTerms) {
this.addOption(creditTerms);
}
}
searchCompanies = filter =>
api.companies
.search({ filter, page: 1, limit: 20 })
.then(resp => resp.data.results);
searchContacts = companyId => {
if (!companyId) {
this.setState({ contacts: [] });
} else {
api.companies.contacts
.search(companyId, {
active: true,
page: 1,
limit: 1000,
sort: "name",
reverse: false
})
.then(resp => {
this.setState({ contacts: resp.data.results });
});
}
};
handleCompanySelected = company => {
this.setState({ selectedCompany: company });
this.resetTerms();
this.props.contact_id.input.onChange(null);
if (company.customer_credit_terms) {
this.updateTerms(company.customer_credit_terms);
}
if (this.props.onCompanySelected) {
this.props.onCompanySelected(company);
}
this.searchContacts(company.id);
};
resetTerms = () => {
this.props.credit_terms.input.onChange(null);
this.setState({ terms: baseOptions });
};
addOption = option => {
this.setState({
terms: baseOptions.concat(option)
});
};
updateTerms = term => {
this.addOption(term);
this.props.credit_terms.input.onChange(term);
};
render() {
const { contacts, terms } = this.state;
const {
canEditCompany = true,
contact_id: { input: { value } }
} = this.props;
const options = pojo(VENDOR_TERMS).pick(...terms);
const contactOptions = (contacts || [])
.map(c => ({
value: c.id,
text: c.name
}))
.concat({
text: "+ Add a Contact",
value: "Add"
});
return (
<div>
<Row className="row-col-gutters-half">
<Col md={8}>
<Field
name="company_id"
label="Customer"
labelChildren={
this.state.selectedCompany && (
<React.Fragment>
<CompanyStatsTooltip
company={this.state.selectedCompany}
/>
</React.Fragment>
)
}
placeholder="Search by typing a company's name or code"
component={AutoCompleteField}
search={this.searchCompanies}
formatItem={result => result && result.name}
onItemSelected={i => i && this.handleCompanySelected(i)}
selectedItem={this.state.selectedCompany}
disabled={!canEditCompany}
required
/>
</Col>
<Col md={4}>
<Field
name="credit_terms"
component={Select}
label="Customer Credit Terms"
options={toOptions(options)}
required
/>
</Col>
</Row>
<Row className="row-col-gutters-half">
<Col md={4}>
<Field
name="contact_id"
component={Select}
label="Customer Contact"
options={contactOptions}
required
/>
</Col>
<Col md={4}>
<Field
name="cc_email_address"
component={FormInput}
label="Contact Email #2"
/>
</Col>
<Col md={4}>
<Field
required
name="customer_purchase_order_number"
component={FormInput}
label="Customer PO#"
/>
</Col>
</Row>
{value === "Add" && (
<div className="row row-col-gutters-half">
<div className="col-md-6">
<div className="form-group">
<Field
name="contact.name"
component={FormInput}
label="Contact Name"
required
/>
</div>
</div>
<div className="col-md-6">
<div className="form-group">
<Field
name="contact.email"
component={FormInput}
label="Contact Email"
required
/>
</div>
</div>
</div>
)}
<Row className="row-col-gutters-half">
<Col md={4}>
<CheckGroup justified title="After Hours">
<Field name="after_hours" component={Check} label="Yes" />
</CheckGroup>
</Col>
<Col md={4}>
<CheckGroup justified title="Send SO Acknowledgement">
<Field
name="send_sales_order_acknowledgement"
component={Check}
label="Yes"
icon="fa-eye-slash text-success"
/>
</CheckGroup>
</Col>
</Row>
</div>
);
}
}
CustomerSection.propTypes = {
company: T.shape({
id: T.number.isRequired,
customer_credit_terms: T.string
}),
creditTerms: T.string,
onCompanySelected: T.func,
credit_terms: F.formField(F.stringInput).isRequired,
canEditCompany: T.bool,
contact_id: F.formField(F.malleable).isRequired,
afterHours: T.bool.isRequired
};
export default withFormFields(CustomerSection, [
"credit_terms",
"contact_id",
"after_hours",
"send_sales_order_acknowledgement"
]);
这是我的测试:
import React from "react";
import expect from "expect";
import { mount } from "enzyme";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { Field, reduxForm } from "redux-form";
import { CheckGroup } from "MODULES/shared/components/Forms";
import CustomerSection from "MODULES/SalesOrders/components/shared/FormSections/CustomerSection/CustomerSection";
describe("<CustomerSection />", () => {
const props = {
company: {
id: 1,
customer_credit_terms: "Net-30"
},
creditTerms: "Net-30",
onCompanySelected: () => {},
canEditCompany: true,
afterHours: true
};
const store = createStore(() => ({}));
const DecoratedCustomerSection = reduxForm({ form: "testForm" })(
CustomerSection
);
const wrapper = mount(
<Provider store={store}>
<DecoratedCustomerSection {...props} />
</Provider>
);
afterEach(() => {
expect.restoreSpies();
});
it("renders the correct Field components", () => {
const fields = wrapper.find(Field);
expect(fields.length).toEqual(9);
expect(
fields.findWhere(n => n.prop("label") === "Customer").length
).toEqual(1);
expect(
fields.findWhere(n => n.prop("label") === "Customer Credit Terms").length
).toEqual(1);
expect(
fields.findWhere(n => n.prop("label") === "Customer Contact").length
).toEqual(1);
expect(
fields.findWhere(n => n.prop("label") === "Contact Email #2").length
).toEqual(1);
expect(
fields.findWhere(n => n.prop("label") === "Customer PO#").length
).toEqual(1);
expect(
fields.findWhere(n => n.prop("label") === "Contact Name").length
).toEqual(1);
expect(
fields.findWhere(n => n.prop("label") === "Contact Email").length
).toEqual(1);
expect(fields.findWhere(n => n.prop("label") === "Yes").length).toEqual(2);
});
it("renders the correct CheckGroup components", () => {
const checkGroup = wrapper.find(CheckGroup);
expect(
checkGroup.findWhere(n => n.prop("title") === "After Hours").length
).toEqual(1);
expect(
checkGroup.findWhere(n => n.prop("title") === "Send SO Acknowledgement")
.length
).toEqual(1);
});
});
这是lib/forms.js
中toOptions()
的代码:
export function toOptions(map, textField = "label") {
return Object.keys(map).map(t => {
const text = textField ? map[t][textField] : map[t];
return { value: t, text };
});
}