0
votes

I'm testing the content of a <Text /> tag in React Native using Enzyme and Jest. My problem is the that the test is failing (even though everything empirically works and even though I feel like I wrote the test correctly). Here is the test:

        describe("when less than minimum mandatory chosen", () => {
          it("should render a label saying choose at least X items", () => {
            console.log(wrapper);
            expect(
              wrapper
                .find(Text)
                .at(1)
                .contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${props.minChoices}.`)
            ).toBe(true);
          });
        });

I would like to check what the actual string within the <Text /> tag is. How could I achieve this?

As per request by Brian, here is the full test code:

import React from "react";
import { shallow } from "enzyme";
import { Text, View } from "react-native";

import {
  SCREEN_TEXT_MENU_ITEM_DETAIL_MANDATORY,
  SCREEN_TEXT_MENU_ITEM_DETAIL_OPTIONAL,
  SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO,
  SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST
} from "../../config/constants/screenTexts";
import { AccordionList } from "./AccordionList";
import styles from "./styles";

const createTestProps = props => ({
  header: "Kekse",
  items: [
    {
      uuid: "1057e751-8ef1-4524-a743-1b4ba7b33d7b",
      name: "Haferkeks",
      price: "2.00",
      priceCurrency: "EUR"
    },
    {
      uuid: "f41f8e1a-b526-490e-ba4a-3d6acb3f3c16",
      name: "Schokosojakeks",
      price: "1.50",
      priceCurrency: "EUR"
    }
  ],
  chosenItems: [],
  onItemPressed: jest.fn(),
  ...props
});

describe("AccordionList", () => {
  describe("rendering", () => {
    let wrapper;
    let props;
    beforeEach(() => {
      props = createTestProps();
      wrapper = shallow(<AccordionList {...props} />);
    });

    it("should render container", () => {
      expect(
        wrapper
          .find(View)
          .at(0)
          .prop("style")
      ).toContain(styles.container);
    });

    it("should render a <Collapsible />", () => {
      expect(wrapper.find("Collapsible")).toHaveLength(1);
    });

    it("should give the header's <TouchableOpacity /> the headerbutton style", () => {
      expect(
        wrapper
          .find("TouchableOpacity")
          .at(0)
          .prop("style")
      ).toEqual(styles.headerButton);
    });

    it("should render a header", () => {
      expect(
        wrapper
          .find(Text)
          .at(0)
          .contains(props.header)
      ).toBe(true);
    });

    it("should give the header the header style", () => {
      expect(
        wrapper
          .find(Text)
          .at(0)
          .prop("style")
      ).toEqual(styles.header);
    });

    it("should render a subheader", () => {
      expect(
        wrapper
          .find(Text)
          .at(1)
          .prop("style")
      ).toContain(styles.subHeader);
    });

    it("should render a <TouchableOpacity /> for each of it's items", () => {
      expect(wrapper.find("TouchableOpacity")).toHaveLength(props.items.length + 1);
    });

    describe("folded", () => {
      it("should render an arrow pointing to the right", () => {
        expect(wrapper.find("Image").prop("source")).toEqual(
          require("../../assets/icons/rightArrow.png")
        );
      });

      it("should render the folded arrow with the default style", () => {
        expect(wrapper.find("Image").prop("style")).toEqual([styles.arrowIcon, styles.inActive]);
      });

      describe("mandatory", () => {
        beforeEach(() => {
          props = createTestProps({ minChoices: 1 });
          wrapper = shallow(<AccordionList {...props} />);
        });

        it("should render a mandatory label with the minimum number of mandatory items", () => {
          expect(
            wrapper
              .find(Text)
              .at(1)
              .contains(`(${SCREEN_TEXT_MENU_ITEM_DETAIL_MANDATORY}, ${props.minChoices})`)
          ).toBe(true);
        });
      });

      describe("optional", () => {
        it("should render an optional label", () => {
          expect(
            wrapper
              .find(Text)
              .at(1)
              .contains(`(${SCREEN_TEXT_MENU_ITEM_DETAIL_OPTIONAL})`)
          ).toBe(true);
        });
      });
    });

    describe("expanded", () => {
      beforeEach(() => {
        wrapper.setState({ collapsed: false });
      });

      it("should render an arrow pointing down", () => {
        expect(wrapper.find("Image").prop("source")).toEqual(
          require("../../assets/icons/downArrow.png")
        );
      });

      it("should render the expanded arrow with the default style", () => {
        expect(wrapper.find("Image").prop("style")).toEqual([styles.arrowIcon, styles.inActive]);
      });

      // FIXME: These tests should also work but don't for some reason.
      describe("mandatory", () => {
        beforeEach(() => {
          props = createTestProps({ minChoices: 1 });
          wrapper = shallow(<AccordionList {...props} />);
          wrapper.setState({ collapsed: false });
        });

        describe("when less than minimum mandatory chosen", () => {
          it("should render a label saying choose at least X items", () => {
            expect(
              wrapper
                .find(Text)
                .at(1)
                .contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${props.minChoices}.`)
            ).toBe(true);
          });
        });

        describe("when more than minimum mandatory chosen", () => {
          beforeEach(() => {
            props = createTestProps({
              minChoices: 1,
              chosenItems: ["1057e751-8ef1-4524-a743-1b4ba7b33d7b"]
            });
            wrapper = shallow(<AccordionList {...props} />);
            wrapper.setState({ collapsed: false });
          });

          it("should render a label saying choose up to X items", () => {
            expect(
              wrapper
                .find(Text)
                .at(1)
                .contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${props.maxChoices}.`)
            ).toBe(true);
          });
        });
      });

      describe("optional", () => {
        it("should render a label saying choose up to X items", () => {
          expect(
            wrapper
              .find(Text)
              .at(1)
              .contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${props.maxChoices}.`)
          ).toBe(true);
        });
      });
    });

    describe("item chosen", () => {
      beforeEach(() => {
        props = createTestProps({ chosenItems: ["1057e751-8ef1-4524-a743-1b4ba7b33d7b"] });
        wrapper = shallow(<AccordionList {...props} />);
        wrapper.setState({ collapsed: false });
      });

      it("should render a checkmark for the item", () => {
        expect(
          wrapper
            .find("Image")
            .at(1)
            .prop("source")
        ).toEqual(require("../../assets/icons/checkmark.png"));
      });

      it("should render the checkmark with the checkmarkIcon and active style", () => {
        expect(
          wrapper
            .find("Image")
            .at(1)
            .prop("style")
        ).toEqual([styles.checkmarkIcon, styles.active]);
      });

      it("should render the folded arrow with the primary style", () => {
        expect(
          wrapper
            .find("Image")
            .at(0)
            .prop("style")
        ).toContain(styles.active);
      });

      it("should render the expanded arrow with the primary style", () => {
        wrapper.setState({ collapsed: false });
        expect(
          wrapper
            .find("Image")
            .at(0)
            .prop("style")
        ).toContain(styles.active);
      });
    });

    describe("max items chosen", () => {
      beforeEach(() => {
        props = createTestProps({
          maxChoices: 1,
          chosenItems: ["1057e751-8ef1-4524-a743-1b4ba7b33d7b"]
        });
        wrapper = shallow(<AccordionList {...props} />);
        wrapper.setState({ collapsed: false });
      });

      it("should disable all items but the chosen", () => {
        expect(
          wrapper
            .find("TouchableOpacity")
            .at(2)
            .prop("disabled")
        ).toEqual(true);
      });
    });
  });

  describe("interaction", () => {
    let wrapper;
    let props;
    beforeEach(() => {
      props = createTestProps();
      wrapper = shallow(<AccordionList {...props} />);
    });

    // FIXME: This test does not work for some reason...
    // describe("pressing the header", () => {
    //   beforeEach(() => {
    //     wrapper.instance().toggleExpanded = jest.fn();
    //     wrapper
    //       .find("TouchableOpacity")
    //       .first()
    //       .prop("onPress")();
    //   });
    //
    //   it("should call the toggleExpanded() instance function", () => {
    //     expect(wrapper.instance().toggleExpanded).toHaveBeenCalledTimes(1);
    //   });
    // });

    describe("pressing an item", () => {
      beforeEach(() => {
        wrapper
          .find("TouchableOpacity")
          .at(1)
          .prop("onPress")();
      });

      it("should call the onItemPressed callback", () => {
        expect(props.onItemPressed).toHaveBeenCalledTimes(1);
      });
    });
  });

  describe("component methods", () => {
    describe("toggleExpanded", () => {
      let wrapper;
      let props;
      beforeEach(() => {
        props = createTestProps();
        wrapper = shallow(<AccordionList {...props} />);
        wrapper.instance().toggleExpanded();
      });

      it("should change the state of the component to collapsed=false", () => {
        expect(wrapper.instance().state.collapsed).toBe(false);
      });
    });
  });
});

And here is the full code of the component:

import React, { PureComponent } from "react";
import { Image, Text, TouchableOpacity, View } from "react-native";
import Collapsible from "react-native-collapsible";
import PropTypes from "prop-types";

import {
  SCREEN_TEXT_MENU_ITEM_DETAIL_MANDATORY,
  SCREEN_TEXT_MENU_ITEM_DETAIL_OPTIONAL,
  SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO,
  SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST
} from "../../config/constants/screenTexts";
import styles from "./styles";

export class AccordionList extends PureComponent {
  static propTypes = {
    header: PropTypes.string.isRequired,
    items: PropTypes.array.isRequired,
    chosenItems: PropTypes.array.isRequired,
    onItemPressed: PropTypes.func.isRequired,
    minChoices: PropTypes.number,
    maxChoices: PropTypes.number,
    borderTop: PropTypes.bool,
    borderBottom: PropTypes.bool
  };

  static defaultProps = {
    minChoices: 0,
    maxChoices: 1,
    borderTop: false,
    borderBottom: false
  };

  state = { collapsed: true };

  toggleExpanded = () => {
    this.setState(state => ({ collapsed: !state.collapsed }));
  };

  renderContent = () => {
    const { items, onItemPressed, chosenItems, maxChoices } = this.props;
    return (
      <View>
        {items.map(item => {
          const disabled =
            !chosenItems.includes(item.uuid) &&
            chosenItems.filter(item => items.map(item => item.uuid).includes(item)).length ===
              maxChoices;
          return (
            <View style={styles.itemContainer} key={item.uuid}>
              <TouchableOpacity
                onPress={() => onItemPressed(item.uuid)}
                style={[styles.itemButton, disabled ? styles.opaque : null]}
                disabled={disabled}
              >
                {item.name && <Text style={styles.itemText}>{item.name}</Text>}
                {item.price && (
                  <View style={styles.priceContainer}>
                    <Text style={styles.sizeText}>{item.label ? `${item.label} ` : ""}</Text>
                    <Text style={styles.sizeText}>
                      {item.size ? `${item.size.size}${item.size.unit}: ` : ""}
                    </Text>
                    <Text style={styles.priceText}>{item.price} €</Text>
                  </View>
                )}
                <View style={styles.checkMarkContainer}>
                  {chosenItems.includes(item.uuid) ? (
                    <Image
                      source={require("../../assets/icons/checkmark.png")}
                      resizeMode="contain"
                      style={[styles.checkmarkIcon, styles.active]}
                    />
                  ) : null}
                </View>
              </TouchableOpacity>
            </View>
          );
        })}
      </View>
    );
  };

  render() {
    const {
      header,
      items,
      maxChoices,
      minChoices,
      chosenItems,
      borderTop,
      borderBottom
    } = this.props;
    const { collapsed } = this.state;
    return (
      <View
        style={[
          styles.container,
          borderTop ? styles.borderTop : null,
          borderBottom ? styles.borderBottom : null
        ]}
      >
        <TouchableOpacity onPress={this.toggleExpanded} style={styles.headerButton}>
          <Text style={styles.header}>{header}</Text>
          <Text style={[styles.subHeader, minChoices > 0 ? styles.mandatory : styles.optional]}>
            {minChoices > 0
              ? collapsed
                ? `(${SCREEN_TEXT_MENU_ITEM_DETAIL_MANDATORY}, ${minChoices})`
                : chosenItems.filter(item => items.map(item => item.uuid).includes(item)).length >=
                  maxChoices
                  ? `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${maxChoices}.`
                  : `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${minChoices}`
              : collapsed
                ? `(${SCREEN_TEXT_MENU_ITEM_DETAIL_OPTIONAL})`
                : `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${maxChoices}.`}
          </Text>
          <Image
            source={
              collapsed
                ? require("../../assets/icons/rightArrow.png")
                : require("../../assets/icons/downArrow.png")
            }
            resizeMode="contain"
            style={[
              styles.arrowIcon,
              chosenItems.length > 0 &&
              chosenItems.some(item => items.map(item => item.uuid).includes(item))
                ? styles.active
                : styles.inActive
            ]}
          />
        </TouchableOpacity>
        <Collapsible collapsed={collapsed}>{this.renderContent()}</Collapsible>
      </View>
    );
  }
}

export default AccordionList;
1
did you mean .at(0)? .html() instead of .contains() probably makes more sense for this. The component being tested as well as the full test code would be helpfulBrian Adams
@brian-lives-outdoors Thank you for your interest in helping and your advice! I'm super stuck. I added the code you request :)J. Hesters

1 Answers

1
votes

Looks like it's just a typo, you're missing a . in the component code.

Change this line:

: `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${minChoices}`

...to this:

: `${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_AT_LEAST} ${minChoices}.`

One other thing I noticed, your component uses a default for maxChoices of 1, but in your test there are two spots where you are referencing props.maxChoices where it hasn't been set. You'll probably want to change the two lines like this:

.contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${props.maxChoices}.`)

..to this:

.contains(`${SCREEN_TEXT_MENU_ITEM_DETAIL_CHOOSE_UP_TO} ${props.maxChoices || 1}.`)

to reflect the default assigned by the component.