Skip to content

Commit

Permalink
feat(toBeFeature): add new matcher
Browse files Browse the repository at this point in the history
Verifies an object is a valid GeoJSON Feature. In addition to the core function and matcher, this
adds a new setup script for 'features', reorganizes the package.json test scripts, and refactors the
good and bad geometry examples from toBeGeometryCollection.test.js into a separate data file for
reuse. Type definitions updated and reorganized. Note: this matcher test has extensive snapshot
testing to begin addressing #32.

Resolves: #24
  • Loading branch information
M-Scott-Lassiter committed Jun 1, 2022
1 parent 5c041c6 commit 551aa7f
Show file tree
Hide file tree
Showing 17 changed files with 1,290 additions and 262 deletions.
1 change: 1 addition & 0 deletions .cz-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const coordinateMatchers = [
{ name: 'isValidBoundingBox' },
{ name: 'isValidCoordinate' },
{ name: 'toBeAnyGeometry' },
{ name: 'toBeFeature' },
{ name: 'toBeGeometryCollection' },
{ name: 'toBeLineStringGeometry' },
{ name: 'toBeMultiLineStringGeometry' },
Expand Down
9 changes: 6 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ This project provides high working confidence to developers by uses Jest itself

```bash
npm run test # Runs all tests and generates coverage report
npm run test:dev # Runs tests in watch mode
npm run test-<type> #runs the tests only in that category
npm run test:<type> #runs the tests only in that category
npm run watch # Runs tests in watch mode
```

### Documentation
Expand Down Expand Up @@ -242,12 +242,15 @@ describe('Error Snapshot Testing', () => {
})
```

To ensure code refactoring doesn't result in [vague error messages](https://github.com/M-Scott-Lassiter/jest-geojson/issues/32), ensure there is at least one snapshot covering each expected error message.

### Updating Error Snapshots

If you change the error messages, the snapshots will also change. Once you manually verify it still works as intended, update the snapshot:
If you change the error messages, the snapshots will also change. Once you manually verify it still works as intended, update the snapshot using one of the following:

```bash
npm run test -- -u
npm run test:<type> -- -u
```

### Export Both Core and Matcher Functions
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ _Future_

_1.0.0_

- [ ] toBeFeature
- [x] toBeFeature

---

Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@
"format": "npx prettier . --write",
"tableofcontents": "markdown-toc -i ./README.md && markdown-toc -i ./CONTRIBUTING.md",
"test": "jest --coverage --verbose",
"test:dev": "jest --watch --coverage --verbose",
"test-coordinates": "jest tests/coordinates --coverage --verbose",
"test-boundingboxes": "jest tests/boundingBoxes --coverage --verbose",
"test-geometries": "jest tests/geometries --coverage --verbose",
"test:coordinates": "jest tests/coordinates --coverage --verbose",
"test:boundingboxes": "jest tests/boundingBoxes --coverage --verbose",
"test:features": "jest tests/features --coverage --verbose",
"test:geometries": "jest tests/geometries --coverage --verbose",
"lint": "eslint . --ext .js --fix",
"prepare": "husky install"
"prepare": "husky install",
"watch": "jest --watch --coverage --verbose"
},
"sideEffects": false,
"config": {
Expand Down
4 changes: 4 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ exports.coordinates = {
validCoordinate: require('./core/coordinates/validCoordinate')
}

exports.features = {
feature: require('./core/features/feature')
}

exports.geometries = {
anyGeometry: require('./core/geometries/anyGeometry'),
geometryCollection: require('./core/geometries/geometryCollection'),
Expand Down
154 changes: 154 additions & 0 deletions src/core/features/feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
const { anyGeometry } = require('../geometries/anyGeometry')
const { pointGeometry } = require('../geometries/pointGeometry')
const { multiPointGeometry } = require('../geometries/multiPointGeometry')
const { lineStringGeometry } = require('../geometries/lineStringGeometry')
const { multiLineStringGeometry } = require('../geometries/multiLineStringGeometry')
const { polygonGeometry } = require('../geometries/polygonGeometry')
const { multiPolygonGeometry } = require('../geometries/multiPolygonGeometry')
const { validBoundingBox } = require('../boundingBoxes/validBoundingBox')

/**
* Verifies an object is a valid GeoJSON Feature. This object requires a "type" member that must
* equal 'Feature', a "geometry" member that contains either one of the seven valid GeoJSON
* geometry objects or an empty array, and a "properties" member that is either an object of any
* composition or null.
*
* Foreign members are allowed with the exceptions thrown below.
* If present, bounding boxes must be valid.
*
* @memberof Core.Features
* @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/24
* @param {object} featureObject a GeoJSON LineString Geometry object
* @param {string} [geometryType] Specific type of geometry to search for
* @returns {boolean} True if a valid GeoJSON Feature. If invalid, it will throw an error.
* @throws {Error} Argument not an object
* @throws {Error} Must have a type property with value 'Feature'
* @throws {Error} Forbidden from having a property 'coordinates', 'geometries', 'properties', or 'features'
* @throws {Error} Bounding box must be valid (if present)
* @throws {Error} ID must be either a number or string (if present)
* @example
* const testFeature = {
* "type": "Feature",
* "bbox": [-10.0, -10.0, 10.0, 10.0],
* "geometry": {
* "type": "Polygon",
* "coordinates": [
* [
* [-10.0, -10.0],
* [10.0, -10.0],
* [10.0, 10.0],
* [-10.0, -10.0]
* ]
* ]
* },
* "properties": {
* "prop0": "value0",
* "prop1": {
* "this": "that"
* }
* }
* }
* const multiPoint = {
* type: "MultiPoint",
* coordinates: [
* [101.0, 0.0],
* [102.0, 1.0]
* ]
* }
*
* const goodExample1 = feature(testFeature)) // true
* const goodExample2 = feature(testFeature, 'Polygon')) // true
*
* const badExample1 = feature(multiPoint)) // throws error
* const badExample2 = feature(testFeature, 'LineString')) // throws error
* const badExample3 = feature(testFeature.geometry, 'Polygon')) // throws error
*/
function feature(featureObject, geometryType) {
if (featureObject.type !== 'Feature') {
throw new Error(`Must have a type property with value 'Feature'`)
}

if ('coordinates' in featureObject) {
throw new Error(
`GeoJSON Feature objects are forbidden from having a property 'coordinates'.`
)
}

if ('geometries' in featureObject) {
throw new Error(
`GeoJSON Feature objects are forbidden from having a property 'geometries'.`
)
}

if ('features' in featureObject) {
throw new Error(`GeoJSON Feature objects are forbidden from having a property 'features'.`)
}

if (!('properties' in featureObject)) {
throw new Error(`GeoJSON Feature objects must have a property 'properties'.`)
}

if (typeof featureObject.properties !== 'object' || Array.isArray(featureObject.properties)) {
throw new Error(`GeoJSON Feature properties must be either null or an object.`)
}

if (!('geometry' in featureObject)) {
throw new Error(`GeoJSON Feature objects must have a property 'geometry'.`)
}

if (typeof featureObject.geometry !== 'object' || Array.isArray(featureObject.geometry)) {
throw new Error(`GeoJSON Feature 'geometry' must be a valid GeoJSON geometry object.`)
}

if ('bbox' in featureObject) {
validBoundingBox(featureObject.bbox)
}

if ('id' in featureObject) {
if (
!(typeof featureObject.id === 'number' || typeof featureObject.id === 'string') ||
Number.isNaN(featureObject.id)
) {
throw new Error(`If present, ID must be either a number or string.`)
}
}

// Guard clause; features are allowed to have null geometry. However, if the matcher explicitly calls
// for a particular geometry type, null isn't an option. We have to check for that.
if (featureObject.geometry === null && geometryType === undefined) {
return true
}

// At this point, we have guaranteed there is a geometry here. Validate it with the core functions.
if (geometryType === undefined) {
anyGeometry(featureObject.geometry)
}

if (geometryType === 'Point') {
pointGeometry(featureObject.geometry)
}

if (geometryType === 'MultiPoint') {
multiPointGeometry(featureObject.geometry)
}

if (geometryType === 'LineString') {
lineStringGeometry(featureObject.geometry)
}

if (geometryType === 'MultiLineString') {
multiLineStringGeometry(featureObject.geometry)
}

if (geometryType === 'Polygon') {
polygonGeometry(featureObject.geometry)
}

if (geometryType === 'MultiPolygon') {
multiPolygonGeometry(featureObject.geometry)
}

return true
}

exports.feature = feature
12 changes: 4 additions & 8 deletions src/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,24 @@

/* eslint-disable global-require */

// Bounding Boxes
exports.boundingBoxes = {
isValid2DBoundingBox: require('./matchers/boundingBoxes/isValid2DBoundingBox')
.isValid2DBoundingBox,
isValid3DBoundingBox: require('./matchers/boundingBoxes/isValid3DBoundingBox')
.isValid3DBoundingBox,
isValidBoundingBox: require('./matchers/boundingBoxes/isValidBoundingBox').isValidBoundingBox
}
// Coordinates

exports.coordinates = {
isValid2DCoordinate: require('./matchers/coordinates/isValid2DCoordinate').isValid2DCoordinate,
isValid3DCoordinate: require('./matchers/coordinates/isValid3DCoordinate').isValid3DCoordinate,
isValidCoordinate: require('./matchers/coordinates/isValidCoordinate').isValidCoordinate
}

// Features

// Feature Collections
exports.features = {
toBeFeature: require('./matchers/features/toBeFeature').toBeFeature
}

// Geometries
exports.geometries = {
toBeAnyGeometry: require('./matchers/geometries/toBeAnyGeometry').toBeAnyGeometry,
toBeGeometryCollection: require('./matchers/geometries/toBeGeometryCollection')
Expand All @@ -38,5 +36,3 @@ exports.geometries = {
toBePointGeometry: require('./matchers/geometries/toBePointGeometry').toBePointGeometry,
toBePolygonGeometry: require('./matchers/geometries/toBePolygonGeometry').toBePolygonGeometry
}

// Functional
101 changes: 101 additions & 0 deletions src/matchers/features/toBeFeature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const { feature } = require('../../core/features/feature')

// eslint-disable-next-line jsdoc/require-returns
/**
* Verifies an object is a valid GeoJSON Feature. This object requires a "type" member that must
* equal 'Feature', a "geometry" member that contains either one of the seven valid GeoJSON
* geometry objects or an empty array, and a "properties" member that is either an object of any
* composition or null.
*
* Foreign members are allowed with the exception of 'coordinates', 'geometries', 'properties', or 'features'.
* If present, bounding boxes must be valid.
*
* @memberof Matchers.Features
* @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/24
* @param {object} featureObject any GeoJSON Feature object
* @param {string} geometryType Optional string representing one of the seven GeoJSON geometry types
* @example
* const testFeature = {
* "type": "Feature",
* "bbox": [-10.0, -10.0, 10.0, 10.0],
* "geometry": {
* "type": "Polygon",
* "coordinates": [
* [
* [-10.0, -10.0],
* [10.0, -10.0],
* [10.0, 10.0],
* [-10.0, -10.0]
* ]
* ]
* },
* "properties": {
* "prop0": "value0",
* "prop1": {
* "this": "that"
* }
* }
* }
*
* test('Object is valid GeoJSON Feature', () => {
* expect(testFeature).toBeFeature()
* expect(testFeature).toBeFeature('Polygon')
* })
* @example
* const multiPoint = {
* type: "MultiPoint",
* coordinates: [
* [101.0, 0.0],
* [102.0, 1.0]
* ]
* }
*
* test('Object is NOT valid GeoJSON Geometry Object', () => {
* expect(multiPoint).not.toBeFeature()
* expect(testFeature).not.toBeFeature('LineString')
* expect(testFeature.geometry).not.toBeFeature('Polygon')
* })
*/
function toBeFeature(featureObject, geometryType) {
const { printReceived, matcherHint } = this.utils
const optionalTypeMessage = () => {
if (geometryType !== undefined) {
return ` with ${geometryType} geometry`
}
return ''
}
const passMessage =
// eslint-disable-next-line prefer-template
matcherHint('.not.toBeFeature', 'FeatureObject', 'GeometryType') +
'\n\n' +
`Expected input to not be a valid GeoJSON feature object` +
optionalTypeMessage() +
`.\n\n` +
`Received: ${printReceived(featureObject)}`

/**
* Combines a custom error message with built in Jest tools to provide a more descriptive error
* meessage to the end user.
*
* @param {string} errorMessage Error message text to return to the user
* @returns {string} Concatenated Jest test result string
*/
function failMessage(errorMessage) {
return (
// eslint-disable-next-line prefer-template, no-unused-expressions
matcherHint('.toBeFeature', 'FeatureObject', 'GeometryType') +
'\n\n' +
`${errorMessage}\n\n` +
`Received: ${printReceived(featureObject)}`
)
}

try {
feature(featureObject, geometryType)
} catch (err) {
return { pass: false, message: () => failMessage(err.message) }
}
return { pass: true, message: () => passMessage }
}

exports.toBeFeature = toBeFeature
1 change: 1 addition & 0 deletions src/setup/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const jestExpect = global.expect
if (jestExpect !== undefined) {
expect.extend(matchers.coordinates)
expect.extend(matchers.boundingBoxes)
expect.extend(matchers.features)
expect.extend(matchers.geometries)
} else {
exports.throwJestRuntimeError()
Expand Down
10 changes: 10 additions & 0 deletions src/setup/features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const matchers = require('../matchers')
const { throwJestRuntimeError } = require('./all')

const jestExpect = global.expect

if (jestExpect !== undefined) {
expect.extend(matchers.features)
} else {
throwJestRuntimeError()
}
Loading

0 comments on commit 551aa7f

Please sign in to comment.