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' }}
|
- 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
|
||||||
|
|
|
@ -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 .",
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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.' },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
Reference in a new issue