diff --git a/.gitignore b/.gitignore index 7c4393cb7..f8c980f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,6 @@ static/uploads cypress/videos cypress/screenshots/ cypress.env.json + +# Apple macOS folder attribute file +.DS_Store diff --git a/components/LocaleSwitch.vue b/components/LocaleSwitch.vue index a326d4616..f69d107a9 100644 --- a/components/LocaleSwitch.vue +++ b/components/LocaleSwitch.vue @@ -12,11 +12,11 @@ @click.prevent="toggleMenu()" > {{ current.code.toUpperCase() }} diff --git a/components/SearchInput.spec.js b/components/SearchInput.spec.js new file mode 100644 index 000000000..2f3f31987 --- /dev/null +++ b/components/SearchInput.spec.js @@ -0,0 +1,144 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import SearchInput from './SearchInput.vue' +import Vuex from 'vuex' +import Styleguide from '@human-connection/styleguide' +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) +localVue.filter('truncate', () => 'truncated string') +localVue.filter('dateTime', () => Date.now) + +describe('SearchInput.vue', () => { + let wrapper + let mocks + let propsData + + beforeEach(() => { + propsData = {} + }) + + describe('mount', () => { + const Wrapper = () => { + mocks = { + $t: () => {} + } + return mount(SearchInput, { mocks, localVue, propsData }) + } + + it('renders', () => { + expect(Wrapper().is('div')).toBe(true) + }) + + it('has id "nav-search"', () => { + expect(Wrapper().contains('#nav-search')).toBe(true) + }) + + it('defaults to an empty value', () => { + expect(Wrapper().vm.value).toBe('') + }) + + it('defaults to id "nav-search"', () => { + expect(Wrapper().vm.id).toBe('nav-search') + }) + + it('default to a 300 millisecond delay from the time the user stops typing to when the search starts', () => { + expect(Wrapper().vm.delay).toEqual(300) + }) + + it('defaults to an empty array as results', () => { + expect(Wrapper().vm.results).toEqual([]) + }) + + it('defaults to pending false, as in the search is not pending', () => { + expect(Wrapper().vm.pending).toBe(false) + }) + + it('accepts values as a string', () => { + propsData = { value: 'abc' } + const wrapper = Wrapper() + expect(wrapper.vm.value).toEqual('abc') + }) + + describe('testing custom functions', () => { + let select + let wrapper + + beforeEach(() => { + wrapper = Wrapper() + select = wrapper.find('.ds-select') + select.trigger('focus') + select.element.value = 'abcd' + }) + + it('opens the select dropdown when focused on', () => { + expect(wrapper.vm.isOpen).toBe(true) + }) + + it('opens the select dropdown and blurs after focused on', () => { + select.trigger('blur') + expect(wrapper.vm.isOpen).toBe(false) + }) + + it('is clearable', () => { + select.trigger('input') + select.trigger('keyup.esc') + expect(wrapper.emitted().clear.length).toBe(1) + }) + + it('changes the unprocessedSearchInput as the value changes', () => { + select.trigger('input') + expect(wrapper.vm.unprocessedSearchInput).toBe('abcd') + }) + + it('searches for the term when enter is pressed', async () => { + select.trigger('input') + select.trigger('keyup.enter') + await expect(wrapper.emitted().search[0]).toEqual(['abcd']) + }) + + it('calls onDelete when the delete key is pressed', () => { + const spy = jest.spyOn(wrapper.vm, 'onDelete') + select.trigger('input') + select.trigger('keyup.delete') + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('calls query when a user starts a search by pressing enter', () => { + const spy = jest.spyOn(wrapper.vm, 'query') + select.trigger('input') + select.trigger('keyup.enter') + expect(spy).toHaveBeenCalledWith('abcd') + }) + + it('calls onSelect when a user selects an item in the search dropdown menu', async () => { + // searched for term in the browser, copied the results from Vuex in Vue dev tools + propsData = { + results: [ + { + __typename: 'Post', + author: { + __typename: 'User', + id: 'u5', + name: 'Trick', + slug: 'trick' + }, + commentsCount: 0, + createdAt: '2019-03-13T11:00:20.835Z', + id: 'p10', + label: 'Eos aut illo omnis quis eaque et iure aut.', + shoutedCount: 0, + slug: 'eos-aut-illo-omnis-quis-eaque-et-iure-aut', + value: 'Eos aut illo omnis quis eaque et iure aut.' + } + ] + } + wrapper = Wrapper() + select.trigger('input') + const results = wrapper.find('.ds-select-option') + results.trigger('click') + await expect(wrapper.emitted().select[0]).toEqual(propsData.results) + }) + }) + }) +}) diff --git a/components/SearchInput.vue b/components/SearchInput.vue new file mode 100644 index 000000000..7ce174a9a --- /dev/null +++ b/components/SearchInput.vue @@ -0,0 +1,289 @@ + + + + + diff --git a/cypress/integration/06.Search.feature b/cypress/integration/06.Search.feature new file mode 100644 index 000000000..0a4450829 --- /dev/null +++ b/cypress/integration/06.Search.feature @@ -0,0 +1,41 @@ +Feature: Search + As a user + I would like to be able to search for specific words + In order to find related content + + Background: + Given I have a user account + And we have the following posts in our database: + | Author | id | title | content | + | Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! | + | Brianna Wiest | p1 | No searched for content | will be found in this post, I guarantee | + Given I am logged in + + Scenario: Search for specific words + When I search for "Essays" + Then I should have one post in the select dropdown + Then I should see the following posts in the select dropdown: + | title | + | 101 Essays that will change the way you think | + + Scenario: Press enter starts search + When I type "Essa" and press Enter + Then I should have one post in the select dropdown + Then I should see the following posts in the select dropdown: + | title | + | 101 Essays that will change the way you think | + + Scenario: Press escape clears search + When I type "Ess" and press escape + Then the search field should clear + + Scenario: Select entry goes to post + When I search for "Essays" + And I select an entry + Then I should be on the post's page + + Scenario: Select dropdown content + When I search for "Essays" + Then I should have one post in the select dropdown + Then I should see posts with the searched-for term in the select dropdown + And I should not see posts without the searched-for term in the select dropdown diff --git a/cypress/integration/common/search.js b/cypress/integration/common/search.js new file mode 100644 index 000000000..4809e8a13 --- /dev/null +++ b/cypress/integration/common/search.js @@ -0,0 +1,69 @@ +import { When, Then } from 'cypress-cucumber-preprocessor/steps' +When('I search for {string}', value => { + cy.get('#nav-search') + .focus() + .type(value) +}) + +Then('I should have one post in the select dropdown', () => { + cy.get('.ds-select-dropdown').should($li => { + expect($li).to.have.length(1) + }) +}) + +Then('I should see the following posts in the select dropdown:', table => { + table.hashes().forEach(({ title }) => { + cy.get('.ds-select-dropdown').should('contain', title) + }) +}) + +When('I type {string} and press Enter', value => { + cy.get('#nav-search') + .focus() + .type(value) + .type('{enter}', { force: true }) +}) + +When('I type {string} and press escape', value => { + cy.get('#nav-search') + .focus() + .type(value) + .type('{esc}') +}) + +Then('the search field should clear', () => { + cy.get('#nav-search').should('have.text', '') +}) + +When('I select an entry', () => { + cy.get('.ds-select-dropdown ul li') + .first() + .trigger('click') +}) + +Then("I should be on the post's page", () => { + cy.location('pathname').should( + 'eq', + '/post/101-essays-that-will-change-the-way-you-think/' + ) +}) + +Then( + 'I should see posts with the searched-for term in the select dropdown', + () => { + cy.get('.ds-select-dropdown').should( + 'contain', + '101 Essays that will change the way you think' + ) + } +) + +Then( + 'I should not see posts without the searched-for term in the select dropdown', + () => { + cy.get('.ds-select-dropdown').should( + 'not.contain', + 'No searched for content' + ) + } +) diff --git a/layouts/default.vue b/layouts/default.vue index 897cc2724..bdb41f8b2 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -1,21 +1,33 @@