All files / shared / index.ts

94.34% Branches 100/106
100.00% Functions 6/6
95.20% Lines 119/125
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
x3
x3
 
x3
 
x282
 
 
 
x282
x282
x282
x282
x282
x282
x282
x192
x282
 
x6
x282
 
 
 
x84
x84
 
 
 
 
x282
x282
x282
x282
x282
x282
x282
x72
x282
x12
x282
x282
 
x3
x73
 
x73
x146
x110
 
x110
 
 
x110
x110
x110
x110
x146
 
x71
x73
 
 
 
 
 
x3
 
 
 
 
 
 
 
x75
x75
 
 
 
x37
x75
x71
x50
x50
x50
 
x50
x71
x37
x75
 
x3
x3
x3
x3
 
x150
 
 
x150
x16
x14
x14
x14
x16
x2
x2
x2
 
x2
x16
x150
x150
x6
x6
x128
x150
 
x3
x3
x3
x3
 
x132
x132
 
 
 
 
x132
x72
 
 
x132
x35
 
 
x35
 
 
 
 
x60
x60
x60
x60
x60
x10
 
 
x25
x6
x6
x6
x6
 
 
x6
 
 
x15
 
x9
x9
x9
x9
 
x1
x1
x9
x132
x132
 
x42
 
x42
 
 
 
x42
 
x7
x7
x7
x7
 
x35
x42



I














I






I

I
I











































I

















































































































const hasOwn = Object.prototype.hasOwnProperty;
const objToString = Object.prototype.toString;

export function objectType(obj: unknown): string {
  if (obj === null) return 'null';
  const t = typeof obj;
  // Fast path: primitives + functions return their `typeof` directly. Skips
  // the Object.prototype.toString.call() detour that allocates a "[object Foo]"
  // string for every primitive check.
  switch (t) {
    case 'undefined':
    case 'string':
    case 'boolean':
    case 'function':
    case 'symbol':
    case 'bigint':
      return t;
    case 'number':
      // x !== x is true only for NaN.
      return obj !== obj ? 'nan' : 'number';
  }
  // typeof === 'object': fall through to qunit.js's exact slow path so
  // boxed primitives, cross-realm objects, and Symbol.toStringTag overrides
  // produce the same classification as the upstream library.
  const type = objToString.call(obj).slice(8, -1);
  switch (type) {
    case 'Number':
      return isNaN(obj as number) ? 'nan' : 'number';
    case 'String':
    case 'Boolean':
    case 'Array':
    case 'Set':
    case 'Map':
    case 'Date':
    case 'RegExp':
    case 'Function':
    case 'Symbol':
      return type.toLowerCase();
    default:
      return 'object';
  }
}

export function objectValues(obj: unknown, allowArray = true): unknown {
  const vals: Record<string, unknown> | unknown[] = allowArray && Array.isArray(obj) ? [] : {};

  for (const key in obj as object) {
    if (hasOwn.call(obj, key)) {
      const val = (obj as Record<string, unknown>)[key];
      // `val === Object(val)` returns true for objects+functions and false for
      // primitives/null — but it boxes every primitive into a wrapper object
      // just to throw it away. The typeof check is the same boolean without
      // the per-call allocation.
      const t = typeof val;
      const isObjectish = val !== null && (t === 'object' || t === 'function');
      (vals as Record<string, unknown>)[key] = isObjectish ? objectValues(val, allowArray) : val;
    }
  }

  return vals;
}

/**
 * Recursively clone an object into a plain object, taking only the
 * subset of own enumerable properties that exist in a given model.
 */
export function objectValuesSubset(obj: unknown, model: unknown): unknown {
  // Return primitive values unchanged to avoid false positives or confusing
  // results from assert.propContains().
  // E.g. an actual null or false wrongly equaling an empty object,
  // or an actual string being reported as object not matching a partial object.
  // `obj !== Object(obj)` boxes primitives into wrapper objects every call —
  // typeof matches the same set without allocating.
  if (obj === null) return obj;
  const objT = typeof obj;
  if (objT !== 'object' && objT !== 'function') return obj;

  // Unlike objectValues(), subset arrays to a plain objects as well.
  // This enables subsetting [20, 30] with {1: 30}.
  const subset: Record<string, unknown> = {};
  for (const key in model as object) {
    if (hasOwn.call(model, key) && hasOwn.call(obj, key)) {
      subset[key] = objectValuesSubset(
        (obj as Record<string, unknown>)[key],
        (model as Record<string, unknown>)[key],
      );
    }
  }
  return subset;
}

export function validateExpectedExceptionArgs(
  expected: unknown,
  message: string | undefined,
  assertionMethod: string,
): [unknown, string | undefined] {
  const expectedType = objectType(expected);

  // 'expected' is optional unless doing string comparison
  if (expectedType === 'string') {
    if (message === undefined) {
      message = expected as string;
      expected = undefined;
      return [expected, message];
    } else {
      throw new Error(
        `assert.${assertionMethod} does not accept a string value for the expected argument.\n` +
        'Use a non-string object value (e.g. RegExp or validator function) instead if necessary.',
      );
    }
  }
  const valid = !expected || expectedType === 'regexp' || expectedType === 'function' || expectedType === 'object';
  if (!valid) {
    throw new Error(`Invalid expected value type (${expectedType}) provided to assert.${assertionMethod}.`);
  }
  return [expected, message];
}

export function validateException(
  actual: unknown,
  expected: unknown,
  message: string | undefined,
): [boolean, unknown, string | undefined] {
  let result = false;
  const expectedType = objectType(expected);

  // These branches should be exhaustive, based on validation done in validateExpectedException

  // We don't want to validate
  if (!expected) {
    result = true;

    // Expected is a regexp
  } else if (expectedType === 'regexp') {
    result = (expected as RegExp).test(errorString(actual));

    // Log the string form of the regexp
    expected = String(expected);

    // Expected is a constructor, maybe an Error constructor.
    // Note the extra check on its prototype - this is an implicit
    // requirement of "instanceof", else it will throw a TypeError.
  } else if (
    expectedType === 'function' &&
    (expected as { prototype: unknown }).prototype !== undefined &&
    actual instanceof (expected as new (...args: unknown[]) => unknown)
  ) {
    result = true;

    // Expected is an Error object
  } else if (expectedType === 'object') {
    result =
      actual instanceof (expected as { constructor: new (...args: unknown[]) => unknown }).constructor &&
      (actual as Error).name === (expected as Error).name &&
      (actual as Error).message === (expected as Error).message;

    // Log the string form of the Error object
    expected = errorString(expected);

    // Expected is a validation function which returns true if validation passed
  } else if (expectedType === 'function') {
    // protect against accidental semantics which could hard error in the test
    try {
      result = (expected as (e: unknown) => boolean).call({}, actual) === true;
      expected = null;
    } catch (e) {
      // assign the "expected" to a nice error string to communicate the local failure to the user
      expected = errorString(e);
    }
  }
  return [result, expected, message];
}

function errorString(error: unknown): string {
  // Use String() instead of toString() to handle non-object values like undefined or null.
  const resultErrorString = String(error);

  // If the error wasn't a subclass of Error but something like
  // an object literal with name and message properties...
  if (resultErrorString.slice(0, 7) === '[object') {
    // Based on https://es5.github.io/#x15.11.4.4
    const name = (error as { name?: string }).name || 'Error';
    const msg = (error as { message?: string }).message;
    return msg ? `${name}: ${msg}` : name;
  }

  return resultErrorString;
}