matchers/features/toHaveNumericID.js

const { hasID } = require('../../core/features/hasID')

// eslint-disable-next-line jsdoc/require-returns
/**
 * Checks if a GeoJSON Feature has a numeric ID. Passes if the Feature object has any numeric ID (no argument provided), or
 * if the ID exactly matches the optional argument (single number or RegExp provided), or any value
 * within an array of any combination of numbers or RegExp.
 *
 * The test fails if the object does not have an ID, or if it has an ID that does not match the SearchID.
 *
 * Passing a string type to SearchID will not pass the test, even if the ID exactly matches.
 *
 * @memberof Matchers.Features
 * @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/38
 * @param {object} featureObject any GeoJSON Feature object
 * @param {number|RegExp|number[]|RegExp[]} [SearchID] Specific value or array of possible values
 * to search for.
 * @example
 * const testFeature = {
 *     type: 'Feature',
 *     id: 456,
 *     geometry: {...},
 *     properties: {...}
 * }
 *
 * test('Feature Has an ID', () => {
 *     expect(testFeature).toHaveNumericID()
 *     expect(testFeature).toHaveNumericID(456)
 *     expect(testFeature).toHaveNumericID([1, 123, 345, /[a-z]+[0-9]+/])
 * })
 * @example
 * const testFeatureNoID = {
 *     type: 'Feature',
 *     geometry: {...},
 *     properties: {...}
 * }
 * const testFeatureStringID = {
 *     type: 'Feature',
 *     id: 'f1,
 *     geometry: {...},
 *     properties: {...}
 * }
 *
 * test('Feature Does not Have an ID', () => {
 *     expect(testFeatureNoID).not.toHaveNumericID()
 *     expect(testFeatureStringID).not.toHaveNumericID()
 *     expect(testFeatureStringID).not.toHaveNumericID('f1')
 * })
 */
function toHaveNumericID(featureObject, SearchID) {
    const { printReceived, matcherHint } = this.utils
    const optionalIDMessage = () => {
        if (SearchID !== undefined && SearchID?.length !== 0) {
            return ` of ${SearchID}`
        }
        return ''
    }
    const passMessage =
        // eslint-disable-next-line prefer-template
        matcherHint('.not.toHaveNumericID', 'FeatureObject', 'SearchID') +
        '\n\n' +
        `Expected input to not be a valid GeoJSON Feature object with number type ID` +
        optionalIDMessage() +
        `.\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('.toHaveNumericID', 'FeatureObject', 'SearchID') +
            '\n\n' +
            `${errorMessage}\n\n` +
            `Received:  ${printReceived(featureObject)}`
        )
    }

    let idIsPresent
    // Provide error handling in case of invalid inputs to the matcher
    try {
        idIsPresent = hasID(featureObject, SearchID)

        if (typeof featureObject.id === 'string') {
            throw new Error('ID is a string, expected a number.')
        }
    } catch (err) {
        return { pass: false, message: () => failMessage(err.message) }
    }

    // Input was valid, now return either pass or fail
    if (idIsPresent) {
        return { pass: true, message: () => passMessage }
    }
    return { pass: false, message: () => failMessage(`Did not find the numeric ID ${SearchID}.`) }
}

exports.toHaveNumericID = toHaveNumericID