Skip to content

Commit 886262d

Browse files
committed
feat(@probitas/expect): allow PropertySatisfying matcher to handle null/undefined values
Previously, toHavePropertySatisfying performed automatic property existence checks and rejected null/undefined values before calling the matcher. This prevented users from validating null values, checking for non-existent properties, or handling broken object paths. Since PropertySatisfying is designed for custom user-defined validation logic, users should have full control over all validation aspects, including property existence checks. This change removes the automatic guards and always invokes the matcher with the actual value (or undefined if the property path is invalid). Changes: - Remove toHaveProperty existence check before matcher execution - Remove ensureNonNullish call that rejected null/undefined - Simplify error handling to only report matcher failures - Add test cases for null values and broken intermediate paths
1 parent 5d997db commit 886262d

File tree

3 files changed

+52
-27
lines changed

3 files changed

+52
-27
lines changed

packages/probitas-expect/mixin/__snapshots__/object_value_mixin_test.ts.snap

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ Context (packages/probitas-expect/mixin/object_value_mixin_test.ts:236:5)
109109
236│ await assertSnapshotWithoutColors(
110110
237│ t,
111111
112-
509│ t,
113-
510│ catchError(() => applied.toHaveUserMatching({ name: "Bob" })).message,
112+
554│ t,
113+
555│ catchError(() => applied.toHaveUserMatching({ name: "Bob" })).message,
114114
│ ^
115-
511│ );'
115+
556│ );'
116116
`;
117117

118118
snapshot[`createObjectValueMixin - toHaveUserMatching with source context 2`] = `
@@ -132,8 +132,8 @@ snapshot[`createObjectValueMixin - toHaveUserMatching with source context 2`] =
132132
251const applied = mixin({ dummy: true });
133133
252await assertSnapshotWithoutColors(
134134
 ┆
135-
[90m526t,[39m
136-
[90m527catchError(() => applied.toHaveUserMatching({ name: "Bob" })).message,[39m
135+
[90m571t,[39m
136+
[90m572catchError(() => applied.toHaveUserMatching({ name: "Bob" })).message,[39m
137137
 │ ^
138-
[90m528│ );[39m'
138+
[90m573│ );[39m'
139139
`;

packages/probitas-expect/mixin/object_value_mixin.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
Any,
55
buildMatchingExpected,
66
buildPropertyExpected,
7-
ensureNonNullish,
87
formatValue,
98
toPascalCase,
109
tryOk,
@@ -318,18 +317,9 @@ export function createObjectValueMixin<
318317
const obj = getter.call(this);
319318

320319
let matcherError: Error | undefined;
321-
let propertyExists = false;
322320

323321
try {
324-
// @std/expect mutates array keyPath, so we need to copy it
325-
const keyPathCopy = Array.isArray(keyPath) ? [...keyPath] : keyPath;
326-
stdExpect(obj).toHaveProperty(keyPathCopy);
327-
propertyExists = true;
328-
const keyPathStr = Array.isArray(keyPath) ? keyPath.join(".") : keyPath;
329-
const value = ensureNonNullish(
330-
getPropertyValue(obj, keyPath),
331-
keyPathStr,
332-
);
322+
const value = getPropertyValue(obj, keyPath);
333323
matcher(value);
334324
} catch (error) {
335325
if (error instanceof Error) {
@@ -344,16 +334,6 @@ export function createObjectValueMixin<
344334
if (isNegated ? passes : !passes) {
345335
const keyPathStr = Array.isArray(keyPath) ? keyPath.join(".") : keyPath;
346336

347-
if (!propertyExists && !isNegated) {
348-
throw createExpectationError({
349-
message:
350-
`Expected ${valueName} property "${keyPathStr}" to exist and satisfy the matcher, but it does not exist`,
351-
expectOrigin: config.expectOrigin,
352-
theme: config.theme,
353-
subject: config.subject,
354-
});
355-
}
356-
357337
throw createExpectationError({
358338
message: isNegated
359339
? `Expected ${valueName} property "${keyPathStr}" to not satisfy the matcher, but it did`

packages/probitas-expect/mixin/object_value_mixin_test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,51 @@ Deno.test("createObjectValueMixin - toHaveUserPropertySatisfying", async (t) =>
492492
).message,
493493
);
494494
});
495+
496+
await t.step("success - null value", () => {
497+
const mixin = createObjectValueMixin(
498+
() => ({ value: null }),
499+
() => false,
500+
{ valueName: "data" },
501+
);
502+
const applied = mixin({ dummy: true });
503+
assertEquals(
504+
applied.toHaveDataPropertySatisfying("value", (v) => {
505+
if (v !== null) throw new Error("Must be null");
506+
}),
507+
applied,
508+
);
509+
});
510+
511+
await t.step("success - nested path with null intermediate", () => {
512+
const mixin = createObjectValueMixin(
513+
() => ({ user: null }),
514+
() => false,
515+
{ valueName: "data" },
516+
);
517+
const applied = mixin({ dummy: true });
518+
assertEquals(
519+
applied.toHaveDataPropertySatisfying("user.profile.age", (v) => {
520+
if (v !== undefined) throw new Error("Must be undefined when intermediate is null");
521+
}),
522+
applied,
523+
);
524+
});
525+
526+
await t.step("success - nested path with undefined intermediate", () => {
527+
const mixin = createObjectValueMixin(
528+
() => ({ user: { profile: undefined } }),
529+
() => false,
530+
{ valueName: "data" },
531+
);
532+
const applied = mixin({ dummy: true });
533+
assertEquals(
534+
applied.toHaveDataPropertySatisfying("user.profile.age", (v) => {
535+
if (v !== undefined) throw new Error("Must be undefined when intermediate is undefined");
536+
}),
537+
applied,
538+
);
539+
});
495540
});
496541

497542
Deno.test("createObjectValueMixin - toHaveUserMatching with source context", async (t) => {

0 commit comments

Comments
 (0)