Web CNLearn - Vue Frontend 6 - Making Our Search Results Pretty With Cards and Testing Vuex Getters

New component? You bet. Vuex store getters? Of course. More testing? Always.

So we are creating a WordCard to use in our SearchResults. It will display the characters of the chinese word found, its pinyin, and maybe the first few definitions. I will use the Card component from PrimeVue. You’ll see that the Card has a header, a title, subtitle, content and footer part. What will each one contain and what do we need to do in order to implement that:

  • Header: think of it as a picture. For now I don’t want to use that.
  • Title: the Chinese simplified characters.
  • Subtitle: the pinyin with tone accents
  • Content: the first two definitions
  • Footer: some buttons (not used for now)

Then, when we click on the WordCard, we will be taken to the Word view page for that word (next post).

Ok so let’s implement that.

Initial Component

<template>
  <div class="p-col">
    <Card class="word-card box" style="width: 25rem; margin-bottom: 2em">
      <template #title> {{ word }} </template>
      <template #content>
        <p>Add definition here.</p>
      </template>
    </Card>
  </div>
</template>

<script>
import Card from 'primevue/card';

export default {
  props: {
    word: String,
  },
  components: {
    Card,
  },
  computed: {},
};
</script>

<style>
.word-card {
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2);
  border-radius: 10px;
  transition: 1s;
}

.word-card a:link,
a:visited {
  color: dimgray;
  text-decoration: none;
}

.word-card:hover {
  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.5);
  background-color: rgba(93, 238, 117, 0.5);
}
</style>

What do we have? Well, it’s a card Component that receives as props a Word which is a string. It then displays that word in the title part of the Card component. Some styling in there too. Let’s modify our SearchResults.vue component to pass this.

In our SearchResults.vue we will have:

<template>
  <div data-test="searchResultsDiv" class="search_results">
    <div
     data-test="searchResultsFound"
     v-if="currentWords.size>0"
     class="container p-grid p-flex-column"
     >
      <WordCard v-for="word of currentWords" :key="word" :word="word" />
    </div>
    <div data-test="searchResultsBlank" v-else>
        <p>Enter some text to get started.</p>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex';
import WordCard from '@/components/WordCard.vue';

export default {
  components: {
    WordCard,
  },
  computed: {
    ...mapState('search', ['currentWords']),
  },
};
</script>

The changes are that we import the WordCard component and then instead of using the <p v-for=.....>, we use a WordCard component and we send word as props. Happy? Good. Before adding more, let’s fix our tests. The SearchResults tests will fail because we no longer have the <p> displaying our results. And are we also adding a test for our WordCard component? Wouldn’t be happy otherwise.

Ok so in our SearchResults.spec.js:

// let's see if we can see the 5 <p></p> items
    const pElements = wrapper.findAll('p');
    // now let's iterate over each pElement and each word in segmentedWordsArray
    // let's zip them together...yes Python I know
    const pElsAndWords = zip(pElements, currentWordsArray);
    pElsAndWords.forEach((pElementAndWord) => {
      const pElement = pElementAndWord[0];
      const word = pElementAndWord[1];
      // now check the text in each one and compare with the word in the array
      expect(pElement.text()).toEqual(word);

This is the parts that will fail. Let’s change it to:

    const cardTitles = wrapper.findAll('.p-card-title');
    // now let's iterate over each pElement and each word in segmentedWordsArray
    // let's zip them together...yes Python I know
    const cardTitlesAndWords = zip(cardTitles, currentWordsArray);
    cardTitlesAndWords.forEach((cardTitleAndWord) => {
      const cardTitle = cardTitleAndWord[0];
      const word = cardTitleAndWord[1];
      // now check the text in each one and compare with the word in the array
      expect(cardTitle.text()).toEqual(word);

And life is happy again.

Initial Tests for the WordCard Component

Let’s also create a test for our WordCard. In a WordCard.spec.js:

import { mount } from '@vue/test-utils';
import WordCard from '@/components/WordCard.vue';
import PrimeVue from 'primevue/config';
import search from '@/store/modules/search';
import { createStore } from 'vuex';
import { nextTick } from 'vue';


const factory = () => {
  const store = createStore({
    modules: {
      search,
    },
  });
  const wrapper = mount(WordCard, {
    props: {
      word: '你好',
    },  
    global: {
      plugins: [PrimeVue, store],
    },
  });
  return { store, wrapper };
};

Ok so everything should look familiar. BUT, notice we are also passing some props to our component. Remember our WordCard requires a String word prop so we send it “你好”. You might wonder why we need the store (but after writing these tests we’ll extract the definitions to show in the component from there, so might as well have it.). Now let’s start writing the initial test.

describe('WordCard', () => {
  test('Checks if the component got mounted and props got passed', async () => {
    const { store, wrapper } = factory();
    expect(wrapper.find('.p-card-title').exists()).toBe(true);
    expect(wrapper.find('.p-card-title').text()).toBe('你好');
    expect(wrapper.find('.p-card-content').exists()).toBe(true);
    expect(wrapper.find('.p-card-content').text()).toBe('Add definition here.');
  });
});

We run npm run test:unit, everything passes and we are still happy.

Now how about displaying the pinyin? What about the definitions? All of that information resides in the state in our Store. We need to GET it from there. So we’re going to write some getters. You might think, shouldn’t we just have a computed method in our component that does that? That would work, yes. But we are likely to use the same methods in other components too. So let’s have a function that we can call from anywhere as a getter in the store.

BUT! before we do that, remember that we had some words that had multiple pronunciations/meanings. At the moment, we are only displaying the words from our currentWords set in the state. Really, we should display all the possibilities. What does that mean? Let’s look at an example.

Let’s say we have the following Set of currentWords:

// Set [ '你', '我', '好', '说']

So we have 4 unique words BUT while the first two only have one pronunciation, the last two each have two. So our cachedWords would look like (with some of the object properties removed for brevity):

// Map { 
//    "你": [
//              {
//                  "simplified": "你",
//                  "pinyin_accent": "nǐ",
//                  "definitions": "you (informal, as opposed to courteous 您(nín))",
//               }
//          ]
//    "我": [
//              {
//                  "simplified": "我",
//                  "pinyin_accent": "wǒ",
//                  "definitions": "I; me; my",
//              }
//          ]
//    "好": [
//              {
//                  "simplified": "好",
//                  "pinyin_accent": "hǎo",
//                  "definitions": "good; well; proper; good to; easy to; very; so; (suffix indicating completion or readiness); (of two people) close; on intimate terms; (after a personal pronoun) hello",
//              },
//              {
//                  "simplified": "好",
//                  "pinyin_accent": "hào",
//                  "definitions": "to be fond of; to have a tendency to; to be prone to",
//              }
//          ]
//    "说": [
//              {
//                  "simplified": "说",
//                  "pinyin_accent": "shuì",
//                  "definitions": "to persuade",
//              },
//              {
//                  "simplified": "说",
//                  "pinyin_accent": "shuō",
//                  "definitions": "to speak; to say; to explain; to scold; to tell off; a theory (typically the last character in a compound, as in 日心说 heliocentric theory)",
//              }
//           ]

Notice that "" and "" have two objects in their list of objects. Yes we could just display the first one (or some way of choosing which one). What I’ll do, however, is display WordCards for every meaning and let the user decide which one they’d like to see further (which will also present a challenge for the WordPage, but that’s a problem for later today me). So how can we show the pinyin in the WordCard subtitle? First, let’s make sure we show enough WordCards to account for all the variations of a Word. What’s the flow?

  1. We iterate through currentWords like we do now.
  2. We then GET the respective word from the cachedWords state which returns a list of objects.
  3. We iterate through that list of objects and display the WordCard that many times while also keeping track of some “position” (index).

Creating Some Getters and Using Them

Ok let’s do that. Let’s first create a getter to return the list of objects associated with a word in our cachedWords state.

In our search module in the Vuex store, we’ll add the following getter:

  getters: {
    getWord: (state) => (simplified) => state.cachedWords.get(simplified),
  },

Ok so if we do ```getWord(‘好’), we’d have (again some properties are omitted for brevity):

//          [
//              {
//                  "simplified": "好",
//                  "pinyin_accent": "hǎo",
//                  "definitions": "good; well; proper; good to; easy to; very; so; (suffix indicating completion or readiness); (of two people) close; on intimate terms; (after a personal pronoun) hello",
//              },
//              {
//                  "simplified": "好",
//                  "pinyin_accent": "hào",
//                  "definitions": "to be fond of; to have a tendency to; to be prone to",
//              }
//          ]

If we wanted to get a specific “version”, we’d also have to pass in the position in the list. So let’s create another getter where we can also pass in the position in the function. Should we just create this getter with an optional argument?…maybe? (if we end up having too many getters, I’ll come back to it) but for now let’s just create another one.

  getters: {
    getWord: (state) => (simplified) => state.cachedWords.get(simplified),
    getSpecificWord: (state) => (simplified, pos) => state.cachedWords.get(simplified)[pos],
  }

Ok so the getSpecificWord takes in two arguments: the simplified word and the position. That way we can get a specific version of it (at the moment the order does not necessarily imply anything, still doing work on the backend on that). Let’s use these getters in our SearchResults and WordCard component.

<template>
  <div data-test="searchResultsDiv" class="search_results">
    <div
     data-test="searchResultsFound"
     v-if="currentWords.size>0"
     class="container p-grid p-flex-column"
     >
      <div v-for="simplifiedWord of currentWords" :key="simplifiedWord">
        <WordCard
         v-for="(word, index) in getWord(simplifiedWord)"
         :key="index"
         :word="word.simplified"
        />
      </div>
    </div>
    <div data-test="searchResultsBlank" v-else>
        <p>Enter some text to get started.</p>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex';
import WordCard from '@/components/WordCard.vue';

export default {
  components: {
    WordCard,
  },
  computed: {
    ...mapState('search', ['currentWords']),
    ...mapGetters('search', ['getWord']),
  },
};
</script>

<style scoped>
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
}
</style>

Notice I am using the mapGetters helper to map the gettrs to local computed properties. Then in the template I iteratore over the currentWords, then I iterate over the list returned by getWord(simplifiedWord) and I pass in the word.simplified as the word. Now we should see as many WordCards for each word as there are objects in the list. Still, we’re not displaying the other information. Let’s display the pinyin and the first 2 definitions for each word in the WordCard.

Improving our WordCard

There are a few days of doing that. We can either get that information in our SearchResults and pass it down as props (either as multiple props or as an object) or we can pass in the information required to get it from the state. For the WordCard, I am going to pass in an object of props. The anchor link that will surround it (eventually) and direct us to the WordPage, however, will receive a word and a position. Why am I doing it this way? For the WordCard, it’s because it’s a component that is only used for displaying data. I don’t want to add logic to it (that will also make it easier to test). BUT you migh say: Vlad, isn’t Search Results supposed to also only display stuff? You are right and I’m still deciding what the best way for that is. (I’m either thinking of extracting the logic back to the Search.vue view component or just leaving it like this. I can see both of them working and both having advantages and disadvantages. For now, I will leave it like this).

Ok so the WordCard and its props. What should it receive? It will get a wordObject that contains: simplified characters, pinyin with tone marks, and the first N definitions. The first two are easy to implement in the v-for. Even the last one, but I don’t really want to put the logic in there and have a string split followed by array indexing in there. Plus, what if at some point we decide to display the first 3 definitions? or only the first? Let’s make it a method on the SearchResults component:

export default {
  components: {
    WordCard,
  },
  methods: {
    getWordObject(simplifiedWord, pos, nDefinitions) {
      const allWords = this.getWord(simplifiedWord);
      const {
        simplified: word,
        pinyin_accent: pinyin,
        definitions: fullDefinitions,
      } = allWords[pos];
      const definitions = fullDefinitions.split(';').slice(0, nDefinitions);
      return { word, pinyin, definitions };
    },
  },
  computed: {
    ...mapState('search', ['currentWords']),
    ...mapGetters('search', ['getWord']),
  },
};

Notice that it uses the getter getWord. Then it destructures it, getting what it needs (and aliases), and then we extract the first N definitions. Since we want to pass these down to our WordCard, let’s add some more props to it. For our WordCard.vue component:

<template>
  <div class="p-col">
    <Card class="word-card box" style="width: 25rem; margin-bottom: 2em">
      <template #title> {{ word }} </template>
      <template #subtitle> {{ pinyin }}</template>
      <template #content>
          <ol class="definitionsList">
              <li class="individualDefinition" v-for="(definition, index) in definitions" :key="index">
                  {{ definition }}
              </li>
          </ol>
      </template>
    </Card>
  </div>
</template>

<script>
import Card from 'primevue/card';

export default {
  props: {
    word: String,
    pinyin: String,
    definitions: Array[String],
  },
  components: {
    Card,
  },
  computed: {},
};
</script>

<style>
.word-card {
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2);
  border-radius: 10px;
  transition: 1s;
}

.word-card a:link,
a:visited {
  color: dimgray;
  text-decoration: none;
}

.word-card:hover {
  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.5);
  background-color: rgba(93, 238, 117, 0.5);
}

.definitionsList {
  list-style-position: outside;
  display: flex;
  flex-direction: column;
  align-items: center;
}
</style>

Now it has word, pinyin and definitions as props. How do we pass these to it from our SearchResults? Rather than binding each property separately, we’ll bind the entire object:

<template>
  <div data-test="searchResultsDiv" class="search_results">
    <div
     data-test="searchResultsFound"
     v-if="currentWords.size>0"
     class="container p-grid p-flex-column"
     >
      <div v-for="simplifiedWord of currentWords" :key="simplifiedWord">
        <WordCard
         v-for="(wordObject, index) in getWord(simplifiedWord)"
         :key="index"
         v-bind="getWordObject(simplifiedWord, index, 2)"
        />
      </div>
    </div>
    <div data-test="searchResultsBlank" v-else>
        <p>Enter some text to get started.</p>
    </div>
  </div>
</template>

Yea I know we are “getting” something twice but I don’t mind :) (for now). Ok, that’s our WordCard component. We won’t add the wrapping anchor links just yet, we first need to create the WordPage view component. That post will be tomorrow.

Testing

Let’s see what’s broken after our changes. The WordCard test is broken and the SearchResults test is broken. Fixing the first one is easy, let’s pass in more props and change the things we are checking for.

import { mount } from '@vue/test-utils';
import WordCard from '@/components/WordCard.vue';
import PrimeVue from 'primevue/config';
import search from '@/store/modules/search';
import { createStore } from 'vuex';
import { zip } from 'lodash';


const wordObject = {
    word: '好',
    pinyin: 'hǎo',
    definitions: ['good', 'well']
};

const factory = () => {
  const store = createStore({
    modules: {
      search,
    },
  });
  const wrapper = mount(WordCard, {
    props: wordObject,
    global: {
      plugins: [PrimeVue, store],
    },
  });
  return { store, wrapper };
};




describe('WordCard', () => {
  test('Checks if the component got mounted', async () => {
    const { wrapper } = factory();
    expect(wrapper.find('.p-card-title').exists()).toBe(true);
    expect(wrapper.find('.p-card-title').text()).toBe('好');
    expect(wrapper.find('.p-card-subtitle').exists()).toBe(true);
    expect(wrapper.find('.p-card-subtitle').text()).toBe('好');
    expect(wrapper.find('.p-card-content').exists()).toBe(true);
    const definitionElements = wrapper.findAll('.individualDefinition');
    const definitions = wordObject.definitions;
    const defElementsAndDefinitions = zip(definitionElements, definitions);
    defElementsAndDefinitions.forEach(([ defElement, definition ]) => {
        expect(defElement.text()).toEqual(definition);
    });
  });
});

We created a wordObject “fixture”, and used that when mounting our WordCard component. Then we checked everything is there as we we would expect.

For the SearchResults it’s a bit more difficult. Remember that the actual component uses getters to display the results, which means we would have to have that information in the store. We could either perform the actual search and let everything get to its “equilibrium” state (I’ve been doing science for too long) OR, and this is what we’ll do, we’ll prepopulate our state: we’ll set our searchString, segmentedWords, cachedWords and currentWords.

We’re only gonna that for our second test. At the beginning(ish) of the SearchResult.spec.js file, I have:

const initialSearchStore = {
  searchString: '我们是你们的朋友',
  segmentedWords: new Map([
    [0, '我们'],
    [1, '是'],
    [2, '你们'],
    [3, '的'],
    [4, '朋友'],
  ]),
  cachedWords: new Map([
    ['我们', [{
      id: 40809,
      simplified: '我们',
      traditional: '我們',
      pinyin_num: 'wo3 men5',
      pinyin_accent: 'wǒ men',
      pinyin_clean: 'wo men',
      pinyin_no_spaces: 'women',
      also_written: '',
      classifiers: '',
      definitions: 'we; us; ourselves; our',
      frequency: 283794,
    }]],
    ['是', [{
      id: 49583,
      simplified: '是',
      traditional: '是',
      pinyin_num: 'shi4',
      pinyin_accent: 'shì',
      pinyin_clean: 'shi',
      pinyin_no_spaces: 'shi',
      also_written: '',
      classifiers: '',
      definitions: 'is; are; am; yes; to be',
      frequency: 957407,
    },
    {
      id: 49601,
      simplified: '是',
      traditional: '昰',
      pinyin_num: 'shi4',
      pinyin_accent: 'shì',
      pinyin_clean: 'shi',
      pinyin_no_spaces: 'shi',
      also_written: '',
      classifiers: '',
      definitions: 'variant of 是(shì); (used in given names)',
      frequency: 957407,
    }]],
    ['你们', [{
      id: 6948,
      simplified: '你们',
      traditional: '你們',
      pinyin_num: 'ni3 men5',
      pinyin_accent: 'nǐ men',
      pinyin_clean: 'ni men',
      pinyin_no_spaces: 'nimen',
      also_written: '',
      classifiers: '',
      definitions: 'you (plural)',
      frequency: 46411,
    }]],
    ['的', [{
      id: 71685,
      simplified: '的',
      traditional: '的',
      pinyin_num: 'de5',
      pinyin_accent: 'de',
      pinyin_clean: 'de',
      pinyin_no_spaces: 'de',
      also_written: '',
      classifiers: '',
      definitions: "of; ~'s (possessive particle); (used after an attribute); (used to form a nominal expression); (used at the end of a declarative sentence for emphasis)",
      frequency: 4540297,
    },
    {
      id: 71686,
      simplified: '的',
      traditional: '的',
      pinyin_num: 'di1',
      pinyin_accent: 'dī',
      pinyin_clean: 'di',
      pinyin_no_spaces: 'di',
      also_written: '',
      classifiers: '',
      definitions: 'see 的士(dī shì)',
      frequency: 4540297,
    },
    {
      id: 71687,
      simplified: '的',
      traditional: '的',
      pinyin_num: 'di2',
      pinyin_accent: 'dí',
      pinyin_clean: 'di',
      pinyin_no_spaces: 'di',
      also_written: '',
      classifiers: '',
      definitions: 'really and truly',
      frequency: 4540297,
    },
    {
      id: 71688,
      simplified: '的',
      traditional: '的',
      pinyin_num: 'di4',
      pinyin_accent: 'dì',
      pinyin_clean: 'di',
      pinyin_no_spaces: 'di',
      also_written: '',
      classifiers: '',
      definitions: 'aim; clear',
      frequency: 4540297,
    }]],
    ['朋友', [{
      id: 51254,
      simplified: '朋友',
      traditional: '朋友',
      pinyin_num: 'peng2 you5',
      pinyin_accent: 'péng you',
      pinyin_clean: 'peng you',
      pinyin_no_spaces: 'pengyou',
      also_written: '',
      classifiers: '个; 位',
      definitions: 'friend',
      frequency: 39168,
    }]],
  ]),
  currentWords: new Set(['我们', '是', '你们', '的', '朋友']),
};

Then, in the second test:

  test('Checks if WordCards are shown', async () => {
    const { store, wrapper } = factory();
    expect(wrapper.find('[data-test="searchResultsDiv"]').exists()).toBe(true);
    expect(wrapper.find('[data-test="searchResultsFound"]').exists()).toBe(false);
    expect(wrapper.find('[data-test="searchResultsBlank"]').exists()).toBe(true);
    // manually set the state of our search module state
    store.state.search = initialSearchStore;

    await nextTick();
    expect(wrapper.find('[data-test="searchResultsDiv"]').exists()).toBe(true);
    expect(wrapper.find('[data-test="searchResultsFound"]').exists()).toBe(true);
    expect(wrapper.find('[data-test="searchResultsBlank"]').exists()).toBe(false);
    // there should be 9 cards shown, so there should be 9 cardTtles
    const cardTitles = wrapper.findAll('.p-card-title');
    expect(cardTitles.length).toEqual(9);
    const cardWords = ['我们', '是', '是', '你们', '的', '的', '的', '的', '朋友'];
    // now let's iterate over each cardWord and each cardTitle
    // let's zip them together...yes Python I know
    const cardTitlesAndWords = zip(cardTitles, cardWords);
    cardTitlesAndWords.forEach(([cardTitle, word]) => {
      // now check the text in each one and compare with the word in the array
      expect(cardTitle.text()).toEqual(word);
    });
  });

And everything is well again :) Note that after creating the store, I changed the state of the search module manually to that object I created. We had to do that because our component used getters…Are you saying I need to test those too??????? I wouldn’t have it any other way. Last few tests and we’re done for the day (with this post at least).

Testing our Vuex Getters

Let’s create a new file called VuexSearchModuleGetters.spec.js. Similarly to the tests for SearchResults.spec.js, we will prepopulate our state. The difference is that we will do that for every test. We will have two tests: one for getWord and one for getSpecificWord.

SO we start out VuexSearchModuleGetters.spec.js as:

import search from '@/store/modules/search';
import { createStore } from 'vuex';
import { zip } from 'lodash';

const initialSearchStore = {
  searchString: '我们是你们的朋友',
  segmentedWords: new Map([
    [0, '我们'],
    [1, '是'],
    [2, '你们'],
    [3, '的'],
    [4, '朋友'],
  ]),
  cachedWords: new Map([
    ['我们', [{
      id: 40809,
      simplified: '我们',
      traditional: '我們',
      pinyin_num: 'wo3 men5',
      pinyin_accent: 'wǒ men',
      pinyin_clean: 'wo men',
      pinyin_no_spaces: 'women',
      also_written: '',
      classifiers: '',
      definitions: 'we; us; ourselves; our',
      frequency: 283794,
    }]],
    ['是', [{
      id: 49583,
      simplified: '是',
      traditional: '是',
      pinyin_num: 'shi4',
      pinyin_accent: 'shì',
      pinyin_clean: 'shi',
      pinyin_no_spaces: 'shi',
      also_written: '',
      classifiers: '',
      definitions: 'is; are; am; yes; to be',
      frequency: 957407,
    },
    {
      id: 49601,
      simplified: '是',
      traditional: '昰',
      pinyin_num: 'shi4',
      pinyin_accent: 'shì',
      pinyin_clean: 'shi',
      pinyin_no_spaces: 'shi',
      also_written: '',
      classifiers: '',
      definitions: 'variant of 是(shì); (used in given names)',
      frequency: 957407,
    }]],
    ['你们', [{
      id: 6948,
      simplified: '你们',
      traditional: '你們',
      pinyin_num: 'ni3 men5',
      pinyin_accent: 'nǐ men',
      pinyin_clean: 'ni men',
      pinyin_no_spaces: 'nimen',
      also_written: '',
      classifiers: '',
      definitions: 'you (plural)',
      frequency: 46411,
    }]],
    ['的', [{
      id: 71685,
      simplified: '的',
      traditional: '的',
      pinyin_num: 'de5',
      pinyin_accent: 'de',
      pinyin_clean: 'de',
      pinyin_no_spaces: 'de',
      also_written: '',
      classifiers: '',
      definitions: "of; ~'s (possessive particle); (used after an attribute); (used to form a nominal expression); (used at the end of a declarative sentence for emphasis)",
      frequency: 4540297,
    },
    {
      id: 71686,
      simplified: '的',
      traditional: '的',
      pinyin_num: 'di1',
      pinyin_accent: 'dī',
      pinyin_clean: 'di',
      pinyin_no_spaces: 'di',
      also_written: '',
      classifiers: '',
      definitions: 'see 的士(dī shì)',
      frequency: 4540297,
    },
    {
      id: 71687,
      simplified: '的',
      traditional: '的',
      pinyin_num: 'di2',
      pinyin_accent: 'dí',
      pinyin_clean: 'di',
      pinyin_no_spaces: 'di',
      also_written: '',
      classifiers: '',
      definitions: 'really and truly',
      frequency: 4540297,
    },
    {
      id: 71688,
      simplified: '的',
      traditional: '的',
      pinyin_num: 'di4',
      pinyin_accent: 'dì',
      pinyin_clean: 'di',
      pinyin_no_spaces: 'di',
      also_written: '',
      classifiers: '',
      definitions: 'aim; clear',
      frequency: 4540297,
    }]],
    ['朋友', [{
      id: 51254,
      simplified: '朋友',
      traditional: '朋友',
      pinyin_num: 'peng2 you5',
      pinyin_accent: 'péng you',
      pinyin_clean: 'peng you',
      pinyin_no_spaces: 'pengyou',
      also_written: '',
      classifiers: '个; 位',
      definitions: 'friend',
      frequency: 39168,
    }]],
  ]),
  currentWords: new Set(['我们', '是', '你们', '的', '朋友']),
};

const factory = () => {
  const store = createStore({
    modules: {
      search,
    },
  });
  // manually set the state of our search module state
  store.state.search = initialSearchStore;
  return store;
};

Notice that now, we are setting our search module state to iitialSearchStore in our factory function, which means that every test will start with a prepopulated state. Yes, at some point (or so I claim), I will put these fixtures in some files that I load so we don’t pollute our test files but that’s a job for future me. Now, onto the actual tests:

describe('Vuex Search Module Getters', () => {
  test('Tests the getWord getter', () => {
    const store = factory();
    const getWord = store.getters['search/getWord'];
    const wordsToTest = ['我们', '是', '你们', '的', '朋友'];
    wordsToTest.forEach((word) => {
      expect(getWord(word)).toEqual(initialSearchStore.cachedWords.get(word));
    });
  });
  test('Tests the getSpecificWord getter', () => {
    const store = factory();
    const getSpecificWord = store.getters['search/getSpecificWord'];
    const wordsToTest = ['我们', '是', '是', '你们', '的', '的', '的', '的', '朋友'];
    const positions = [0, 0, 1, 0, 0, 1, 2, 3, 0];
    const wordsAndPositions = zip(wordsToTest, positions);
    wordsAndPositions.forEach(([word, position]) => {
      expect(getSpecificWord(word, position))
        .toEqual(initialSearchStore.cachedWords.get(word)[position]);
    });
  });
});

Please note how I am calling the getters. They are saved as properties of some getters object. The key is the name and the value is the anonymous function. I use lodash’s zip function to iterate over all the possible tests.

That was a lot we did today! Well done us.

The commit for this post is here. Now let’s do some Python, enough JS for today.