feat: toHaveLength matcher (#25)

* feat: toHaveLength matcher

* refactor: standardize errors thrown on failed expectation

* test: toHaveLength coverage

* fix: resolve type discrepancy

* ci: add typecheck step
This commit is contained in:
Marc 2023-04-15 16:30:05 -04:00 committed by GitHub
parent e6ef369fdb
commit 6a9e9ee3fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 109 additions and 4 deletions

View file

@ -45,6 +45,26 @@ jobs:
- if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
run: corepack enable && yarn run: corepack enable && yarn
typecheck:
name: Typecheck
runs-on: ubuntu-latest
needs: dependencies
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: actions/cache@v3
id: dependencies-cache
env:
cache-name: dependencies-cache
with:
path: .yarn
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-18
- run: |
corepack enable && yarn
yarn types:check
integration: integration:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: dependencies needs: dependencies

View file

@ -16,6 +16,7 @@
"prebuild": "rm -rf dist", "prebuild": "rm -rf dist",
"lint": "rome format src tests && rome check src tests", "lint": "rome format src tests && rome check src tests",
"lint:fix": "rome format src tests --write && rome check src tests --apply", "lint:fix": "rome format src tests --write && rome check src tests --apply",
"types:check": "yarn tsc --project . --noEmit",
"test": "$SHELL ./script/test-suite", "test": "$SHELL ./script/test-suite",
"test:integration": "$SHELL ./script/integration-tests", "test:integration": "$SHELL ./script/integration-tests",
"build:ts": "tsc --project .", "build:ts": "tsc --project .",

View file

@ -63,7 +63,7 @@ class Expect<ValueType> {
const out = (matcher as RawNoArgMatcher)(this.value, negated) const out = (matcher as RawNoArgMatcher)(this.value, negated)
if (!out.pass) { if (!out.pass) {
throw new TestAssertionFailed(out.message) assert.fail(out.message)
} }
}) as NoArgMatcher }) as NoArgMatcher
} else if (matcher.length === 2) { } else if (matcher.length === 2) {
@ -71,7 +71,7 @@ class Expect<ValueType> {
const out = (matcher as RawComparisonMatcher)(this.value, other, negated) const out = (matcher as RawComparisonMatcher)(this.value, other, negated)
if (!out.pass) { if (!out.pass) {
throw new TestAssertionFailed(out.message) assert.fail(out.message)
} }
}) as ComparisonMatcher }) as ComparisonMatcher
} }

View file

@ -6,7 +6,7 @@
*/ */
import assert from 'assert' import assert from 'assert'
import { type MatcherReport } from '../types' import { type MatcherReport, type WithLength } from '../types'
/* /*
* Asserts whether value and other are strictly equal. * Asserts whether value and other are strictly equal.
@ -97,4 +97,35 @@ function toNotThrow(func: () => unknown, negated: boolean = false): MatcherRepor
return out return out
} }
export default [toEqual, toBe, toThrow, toNotEqual, toNotBe, toNotThrow] /*
* Validates that the `value` has a length of `length`. The value provided to `value` should
* have a defined length (i.e. it can be a string or some sort of iterable).
*/
function toHaveLength(value: unknown, length: unknown, negated: boolean = false): MatcherReport {
let valueLength = 0
const typedLength = length as number
const typedValue = value as WithLength
if (typeof typedValue === 'string' || typeof typedValue.length === 'number') valueLength = typedValue.length as number
else if (typeof typedValue.length === 'function') valueLength = typedValue.length()
else if (typeof typedValue.size === 'number') valueLength = typedValue.size
else if (typeof typedValue.size === 'function') valueLength = typedValue.size()
else assert.fail(`${value} does not have a known length.`)
const pass = (valueLength === typedLength && !negated) || (valueLength !== typedLength && negated)
if (!negated) {
return {
pass,
message: pass ? '' : `${value} has length ${valueLength}, not ${typedLength}.`,
}
}
return {
pass,
message: pass ? '' : `${value} has length ${typedLength}.`,
}
}
export default [toEqual, toBe, toThrow, toNotEqual, toNotBe, toNotThrow, toHaveLength]

View file

@ -61,3 +61,8 @@ export interface WorkerReport {
export interface CollectorReport { export interface CollectorReport {
totalCases: number totalCases: number
} }
export interface WithLength {
length?: number | (() => number)
size?: number | (() => number)
}

View file

@ -57,3 +57,51 @@ test('Identity negation', () => {
test('Identity negation (fail)', () => { test('Identity negation (fail)', () => {
assert.throws(() => expect('yes').not.toBe('yes')) assert.throws(() => expect('yes').not.toBe('yes'))
}) })
describe('toHaveLength', () => {
test.each([
'word',
[1, 2, 3, 4],
new Set([1, 2, 3, 4]),
new Map([
[1, 1],
[2, 2],
[3, 3],
[4, 4],
]),
])('Asserts length correctly (value=%s)', (value: unknown) => {
assert.doesNotThrow(() => expect(value).toHaveLength(4))
})
test.each([
'word',
[1, 2, 3, 4],
new Set([1, 2, 3, 4]),
new Map([
[1, 1],
[2, 2],
[3, 3],
[4, 4],
]),
])('Asserts length mismatch correctly when negated (value=%s)', (value: unknown) => {
assert.doesNotThrow(() => expect(value).not.toHaveLength(5))
})
test('Fails if the value has no length or size', () => {
assert.throws(
() => {
expect(123).toHaveLength(1)
},
{ name: 'AssertionError', message: '123 does not have a known length.' },
)
})
test('Fails if the provided value is not accurate', () => {
assert.throws(
() => {
expect('word').toHaveLength(1)
},
{ name: 'AssertionError', message: 'word has length 4, not 1.' },
)
})
})