Skip to content

Commit

Permalink
New faceted search results component (#132)
Browse files Browse the repository at this point in the history
* Bare initial code for new faceted search component

* Add Cypress to this component

* Copy cypress test from current faceted search result - WIP

* Use fix version (1.2.1) from `random-words` module

as the upgraded version (1.3.0) was erred with webpack loader error
TODO: fix this later

* Add CheckboxFacetGroup component

* Removed unneeded test folder as we use Cypress for testing

* Add CheckboxFacetGroup component

* Improve CheckboxFacetGroup component

* Add FilterSidebar component

* Add MultiselectDropdownFacetGroup component

* Add MultiselectDropdownFacetGroup to FacetGroups cypress tests

* Add MultiselectDropdownFacetGroup to FilterSidebar component

* Fix name and description of the facet groups

Randomise species, cell types and organism parts

* Add FilterList component without fetch loader but processing new response structure from backend

* Add FacetedSearchContainer component - WIP (need to fix onChange propagation)

* Fix some failing test in FacetedSearchContainer cypress test - 1 is still failing
WIP

* Fix FacetedSearchContainer - its test is passing now

TODO: improve the test, clean the code

* Fix CheckboxFacetGroup when we select multiply checkbox

* Add missing fixtures

* Fix queryParams prop type definition

* Fix the checkboxes checked state in Checkbox facet group

* Add html demo with real data for the new faceted search result

* Fix species checkbox group behaviour

WIP for the other groups

* Fix multi select dropdown facet groups behaviour

* Fix is marker gene behaviour

* Clean the code

* Fix deleting an already selected facet or all of the facets

* Fix adding/deleting filter options that contains comma in their names

* Fix cypress tests in CheckboxFacetGroup

* Fix cypress tests in CheckboxFacetGroup and in MultiSelectDropdownFacetGroup

* Fix cypress tests in FacetedSearchContainer.cy.js

* Add header config to webpack configuration

* Add code coverage for cypress tests

* Update README.md

* Optimising REST calls

Not calling the endpoint of the currently modified facet group

* Reorganising tests for facet groups

* Apply code review findings for FacetedSearchContainer test

* Apply code review findings for package.json

* Apply code review findings

* Apply code review findings for README

* Apply code review findings for FilterList and convert it to stateless

* Apply code review findings for CheckboxFacetGroup and convert it to stateless

* Replace id with role

* Fix the state modification in FacetedSearchController

* Replace ids from the standard vocabulary for roles

* Remove unneeded coveralls dependency

* Update webpack version to ^5.73

* Clean and simplify map usage

Co-authored-by: Alfonso Muñoz-Pomer Fuentes <[email protected]>

* Fix the cleaned code in the previous commit :-)

* Update Cypress to the latest version

* Simplify more facet groups default properties

* Generalise facet group initialization

* Update the `scripts` section of package.json

* Replaced double quotes to single one in the import section

* Add back `webpack-cli` as a dev dependency and fix the README

* Wait for response of the result list + add general interception for each component

* Fix onChange property definition

* Remove sorting from the demo page as we are not using it

* Add ReactToolTip to properly show the tooltip on the page

* Update cypress version

Co-authored-by: Alfonso Muñoz-Pomer Fuentes <[email protected]>

* Fix quotes

Co-authored-by: Alfonso Muñoz-Pomer Fuentes <[email protected]>

---------

Co-authored-by: Alfonso Muñoz-Pomer Fuentes <[email protected]>
  • Loading branch information
ke4 and alfonsomunozpomer authored Oct 13, 2023
1 parent 1ac1630 commit 0b98a0f
Show file tree
Hide file tree
Showing 48 changed files with 35,655 additions and 7 deletions.
1,746 changes: 1,740 additions & 6 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^5.1.2"
"lerna": "^5.1.2",
"webpack-cli": "^5.1.4"
}
}
4 changes: 4 additions & 0 deletions packages/scxa-faceted-search-results-v5/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"plugins": [ "transform-class-properties", "istanbul" ]
}
6 changes: 6 additions & 0 deletions packages/scxa-faceted-search-results-v5/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
webpack.config.js
serve.config.js
coverage/
__mocks__/
lib/
dist/
3 changes: 3 additions & 0 deletions packages/scxa-faceted-search-results-v5/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "@ebi-gene-expression-group"
}
67 changes: 67 additions & 0 deletions packages/scxa-faceted-search-results-v5/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# IntelliJ IDEA project files
.idea/

# https://github.com/github/gitignore/blob/master/Node.gitignore

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# Build files
dist/
lib/
6 changes: 6 additions & 0 deletions packages/scxa-faceted-search-results-v5/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Ignore everything
*
.*

# But the build files...
!lib/**
127 changes: 127 additions & 0 deletions packages/scxa-faceted-search-results-v5/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Faceted Search Results for Single Cell Expression Atlas

[![Build Status](https://travis-ci.org/ebi-gene-expression-group/scxa-faceted-search-results.svg?branch=master)](https://travis-ci.org/ebi-gene-expression-group/scxa-faceted-search-results) [![Coverage Status](https://coveralls.io/repos/github/ebi-gene-expression-group/scxa-faceted-search-results/badge.svg?branch=master)](https://coveralls.io/github/ebi-gene-expression-group/scxa-faceted-search-results?branch=master)

A lightweight and extensible component to list and filter lists of search results. It receives a URL (as a combination
of `host` and `resource`) to async-fetch the results, and a React component class to visualise each element (e.g. a
card).

The facets are used to render a sidebar on the left as a set of filtering controls.
Filter groups can be displayed either as searchable, multiselect dropdown lists or as checkboxes.

Two more optional props, namely `noResultsMessageFormatter` and `resultsMessageFormatter`, return a string which is
displayed when there are no results, and as a header on top of the search results, respectively. In many occasions it
is useful to display such messages using information returned by the server; that’s the reason why both functions take
the JSON payload as an argument, allowing for some customisation in that regard. For example, if in the case of no
results the server returns a field `reason` you could have something like this:
```
<ReactFacetedSearch
...
noResultsMessageFormatter={(data) => `Your search yielded no results: ${data.reason}`}
```

# Requirements

You should have at least Node 16 LTS with NPM 8 or later.
You can check your current installed version executing this command.
```shell
npm --version
8.5.6
```

## How to execute the unit tests with Cypress

We are using the [Cypress framework](https://docs.cypress.io/guides/overview/why-cypress) for unit testing this package.
You can execute the existing tests in the following way:
1. Type `npx cypress open` in the terminal in the root folder of this package.
2. In the appearing browser window you have to click on `Component Testing`,
select a browser and click on `Start Component Testing in <browser name>`.
3. In the appearing list just click on the test you would like to run and check the results on the screen.

## Try out the component with a given example
Just run [webpack-dev-server](https://github.com/webpack/webpack-dev-server):
```
npx webpack serve --mode=development
```

## How to run test with code coverage in the console

Run tests with:

```shell
npx cypress run --component
```

If you want to omit video recording:

```shell
npx cypress run --component --record false
```

Find coverage reports at: `./coverage/lcov-report/index.html`


## Add code coverage (in a nutshell)

This is an abridged version of https://docs.cypress.io/guides/tooling/code-coverage.

Add the following three packages:
```bash
npm install -D babel-plugin-transform-class-properties babel-plugin-istanbul @cypress/code-coverage
```

Add the two plugins to your `.babelrc` file:
```json
{
"presets": ["@babel/preset-react", "@babel/preset-env"],
"plugins": [ "transform-class-properties", "istanbul" ]
}
```

Add `@cypress/code-coverage` in your `cypress.config.js` file so it looks like this:
```js
const { defineConfig } = require(`cypress`)

module.exports = defineConfig({
component: {
setupNodeEvents(on, config) {
require(`@cypress/code-coverage/task`)(on, config)
// include any other plugin code...

// It's IMPORTANT to return the config object
// with any changed environment variables
return config
},
devServer: {
framework: `react`,
bundler: `webpack`
}
}
})
```

Lastly, add the following import to `cypress/support/component.js`:
```js
import '@cypress/code-coverage/support'
```

# Combining with the EBI Visual Framework
React-Select allows you to style the `div` that encloses the `input` element, but not the `input` element itself.
Therefore it’s convenient to add the snippet below to override the styling set by the
[EBI Visual Framework](https://github.com/ebiwd/EBI-Framework) for `input` elements.
```
.input-clear input, .input-clear input:focus {
height: unset;
box-shadow: none;
margin: 0;
}
```
At the time of writing this applies to the most recent version (v1.3). Wrapping these styles within the appropriate
`div` selector is sufficient. See
https://github.com/ebi-gene-expression-group/scxa-faceted-search-results/blob/master/html/filter-list.html for a
working example.

# A note about building with Webpack
The component uses `async`/`await` to fetch the JSON payload from the server. This requires to prefix the entry with
`@babel/polyfill`. If you are already using an equivalent polyfill or
[Runtime transform](http://babeljs.io/docs/plugins/transform-runtime/) you may skip this.
18 changes: 18 additions & 0 deletions packages/scxa-faceted-search-results-v5/cypress.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { defineConfig } = require(`cypress`)

module.exports = defineConfig({
component: {
setupNodeEvents(on, config) {
require(`@cypress/code-coverage/task`)(on, config)
// include any other plugin code...

// It's IMPORTANT to return the config object
// with any changed environment variables
return config
},
devServer: {
framework: `react`,
bundler: `webpack`
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react'

import CheckboxFacetGroup from '../../src/facetgroups/CheckboxFacetGroup'

import { getFacets, getPropsForCheckBoxGroupWithTooltip, getPropsWithoutTooltip, getRandomInt } from './TestUtils'
import vindicators from '../fixtures/vindicatorsResponse.json'

const props = {
name: `Vindicators`,
description: `Show the vindicators`,
facets: [],
queryParams: []
}

describe(`CheckboxFacetGroup`, () => {
beforeEach(() => {
while (props.facets.length === 0) {
props.facets = vindicators.filter(() => Math.random() > 0.5)
}
props.facets = props.facets.map(e => ({ ...e, disabled: false }))
})

it(`displays the expected tooltip if it exists`, () => {
const props = {
...getPropsForCheckBoxGroupWithTooltip(),
facets: getFacets()
}

cy.mount(<CheckboxFacetGroup {...props }/>)
cy.get(`div.padding-bottom-xlarge h4 sup span`)
.should(`have.class`, `icon icon-generic`)
.should(`have.attr`, `data-tip`).then((dataTip) => {
expect(dataTip.toString()
.replace(`<span>`, ``)
.replace(`</span>`, ``)
.replace(`<br>`, ` `))
.to.equal(props.description)
})
})

it(`doesn't display tooltip if not present`, () => {
const propsWithoutTooltip = {
...getPropsWithoutTooltip(),
facets: getFacets()
}
cy.mount(<CheckboxFacetGroup {...propsWithoutTooltip}/>)
cy.get(`div.padding-bottom-xlarge h4`)
.children()
.should(`have.length`, 0)
})

it(`renders the right number of checkboxes and the facet name`, () => {
cy.mount(<CheckboxFacetGroup {...props} />)

cy.get(`input`).should((inputs) => {
expect(inputs.length).to.equal(props.facets.length)
})
cy.get(`input[type="checkbox"]`).should((checkboxes) => {
expect(checkboxes.length).to.equal(props.facets.length)
})
cy.get(`h4`).should(`contains.text`, props.name)
cy.get(`input`).should(`not.have.attr`, `disabled`)
cy.get(`label`).should(`not.have.attr`, `color`)
})

it(`displays disabled check boxes greyed out`, () => {
props.facets = props.facets.map(e => ({ ...e, disabled: true }))
cy.mount(<CheckboxFacetGroup {...props} />)

cy.get(`input`).should(`have.attr`, `disabled`)
cy.get(`label`)
.should(`have.attr`, `style`)
.should(`eq`, `color: lightgrey;`)
})

it(`callback is called when a checkbox is checked/unchecked with the right arguments`, () => {
const randomCheckboxIndex = getRandomInt(0, props.facets.length)
const mockCallbackWrapper = {
onChange: function (facetGroup, checkedFacets) {
}
}
cy.spy(mockCallbackWrapper, `onChange`).as(`onChange`)

cy.mount(<CheckboxFacetGroup {...props} onChange={mockCallbackWrapper.onChange} />)

cy.get(`input[type="checkbox"]`).eq(randomCheckboxIndex).click()
cy.get(`@onChange`)
.should(`have.been.calledOnceWithExactly`,
props.name,
[props.facets[randomCheckboxIndex]],
props.facets
)

cy.get(`input[type="checkbox"]`).eq(randomCheckboxIndex).click()
cy.get(`@onChange`)
.should(`have.been.callCount`, 2)
.should(`have.been.calledWith`,
props.name, [props.facets[randomCheckboxIndex]]
)
.should(`have.been.calledWith`,
props.name, [], props.facets
)
})
})
Loading

0 comments on commit 0b98a0f

Please sign in to comment.