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:
parent
e6ef369fdb
commit
6a9e9ee3fc
6 changed files with 109 additions and 4 deletions
20
.github/workflows/nodejs.yml
vendored
20
.github/workflows/nodejs.yml
vendored
|
@ -45,6 +45,26 @@ jobs:
|
|||
- if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
needs: dependencies
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"prebuild": "rm -rf dist",
|
||||
"lint": "rome format src tests && rome check src tests",
|
||||
"lint:fix": "rome format src tests --write && rome check src tests --apply",
|
||||
"types:check": "yarn tsc --project . --noEmit",
|
||||
"test": "$SHELL ./script/test-suite",
|
||||
"test:integration": "$SHELL ./script/integration-tests",
|
||||
"build:ts": "tsc --project .",
|
||||
|
|
|
@ -63,7 +63,7 @@ class Expect<ValueType> {
|
|||
const out = (matcher as RawNoArgMatcher)(this.value, negated)
|
||||
|
||||
if (!out.pass) {
|
||||
throw new TestAssertionFailed(out.message)
|
||||
assert.fail(out.message)
|
||||
}
|
||||
}) as NoArgMatcher
|
||||
} else if (matcher.length === 2) {
|
||||
|
@ -71,7 +71,7 @@ class Expect<ValueType> {
|
|||
const out = (matcher as RawComparisonMatcher)(this.value, other, negated)
|
||||
|
||||
if (!out.pass) {
|
||||
throw new TestAssertionFailed(out.message)
|
||||
assert.fail(out.message)
|
||||
}
|
||||
}) as ComparisonMatcher
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import assert from 'assert'
|
||||
|
||||
import { type MatcherReport } from '../types'
|
||||
import { type MatcherReport, type WithLength } from '../types'
|
||||
|
||||
/*
|
||||
* Asserts whether value and other are strictly equal.
|
||||
|
@ -97,4 +97,35 @@ function toNotThrow(func: () => unknown, negated: boolean = false): MatcherRepor
|
|||
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]
|
||||
|
|
|
@ -61,3 +61,8 @@ export interface WorkerReport {
|
|||
export interface CollectorReport {
|
||||
totalCases: number
|
||||
}
|
||||
|
||||
export interface WithLength {
|
||||
length?: number | (() => number)
|
||||
size?: number | (() => number)
|
||||
}
|
||||
|
|
|
@ -57,3 +57,51 @@ test('Identity negation', () => {
|
|||
test('Identity negation (fail)', () => {
|
||||
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.' },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
Reference in a new issue