Skip to content
File

Blob: src/wpt/harness/assertions.ts

typescript922 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4// Copyright © web-platform-tests contributors. BSD license
5// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26import {
27 strictEqual,
28 notStrictEqual,
29 deepStrictEqual,
30 ok,
31 throws,
32 fail,
33 rejects,
34 match,
35 type AssertPredicate,
36} from 'node:assert';
37 
38import { type Test } from './test';
39import { sanitize_unpaired_surrogates } from './common';
40 
41declare global {
42 var AssertionError: new (message: string) => Error;
43 
44 function assert_equals(a: unknown, b: unknown, message?: string): void;
45 function assert_not_equals(a: unknown, b: unknown, message?: string): void;
46 function assert_true(val: unknown, message?: string): void;
47 function assert_false(val: unknown, message?: string): void;
48 function assert_array_equals(
49 actual: unknown[],
50 expected: unknown[],
51 description?: string
52 ): void;
53 function assert_object_equals(a: unknown, b: unknown, message?: string): void;
54 function assert_implements(condition: unknown, description?: string): void;
55 function assert_implements_optional(
56 condition: unknown,
57 description?: string
58 ): void;
59 function assert_unreached(description?: string): void;
60 function assert_throws_js(
61 constructor: AssertPredicate,
62 func: ThrowingFn,
63 description?: string
64 ): void;
65 function assert_throws_exactly(
66 exception: AssertPredicate,
67 fn: ThrowingFn,
68 description?: string
69 ): void;
70 function assert_throws_dom(
71 type: number | string,
72 funcOrConstructor: ThrowingFn | typeof DOMException,
73 descriptionOrFunc: string | ThrowingFn,
74 maybeDescription?: string
75 ): void;
76 function assert_throws_quotaexceedederror(
77 func: ThrowingFn,
78 description?: string
79 ): void;
80 function promise_rejects_dom(
81 test: Test,
82 type: number | string,
83 promiseOrConstructor: Promise<unknown> | typeof DOMException,
84 descriptionOrPromise: Promise<unknown> | string,
85 maybeDescription?: string
86 ): Promise<unknown>;
87 
88 function assert_own_property(
89 object: object,
90 property_name: string | symbol,
91 description?: string
92 ): void;
93 function assert_not_own_property(
94 object: object,
95 property_name: string | symbol,
96 description?: string
97 ): void;
98 function promise_rejects_js(
99 test: Test,
100 constructor: typeof Error,
101 promise: Promise<unknown>,
102 description?: string
103 ): Promise<void>;
104 function assert_regexp_match(
105 actual: string,
106 expected: RegExp,
107 description?: string
108 ): void;
109 function assert_greater_than(
110 actual: number,
111 expected: number,
112 description?: string
113 ): void;
114 
115 function assert_greater_than_equal(
116 actual: number,
117 expected: number,
118 description?: string
119 ): void;
120 
121 function assert_less_than(
122 actual: number,
123 expected: number,
124 description?: string
125 ): void;
126 
127 function assert_less_than_equal(
128 actual: number,
129 expected: number,
130 description?: string
131 ): void;
132 
133 function promise_rejects_exactly(
134 test: Test,
135 exception: typeof Error,
136 promise: Promise<unknown>,
137 description?: string
138 ): Promise<void>;
139 
140 function assert_in_array(
141 actual: unknown,
142 expected: unknown[],
143 description?: string
144 ): void;
145 
146 function assert_class_string(
147 object: unknown,
148 class_string: string,
149 description?: string
150 ): void;
151 
152 function assert_inherits(
153 object: unknown,
154 property_name: string,
155 description?: string
156 ): void;
157 
158 function assert_idl_attribute(
159 object: unknown,
160 property_name: string,
161 description?: string
162 ): void;
163 
164 function assert_readonly(
165 object: Record<PropertyKey, unknown>,
166 property_name: string,
167 description?: string
168 ): void;
169}
170 
171type ThrowingFn = () => unknown;
172 
173/**
174 * Exception type that represents a failing assert.
175 * NOTE: This a custom error type defined by WPT - it's not the same as node:assert's AssertionError
176 * @param message - Error message.
177 */
178declare class AssertionError extends Error {}
179 
180function AssertionError(this: AssertionError, message: string): void {
181 if (typeof message == 'string') {
182 message = sanitize_unpaired_surrogates(message);
183 }
184 this.message = message;
185}
186 
187// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- eslint doesn't like "old-style" classes. Code is copied from WPT
188AssertionError.prototype = Object.create(Error.prototype);
189 
190globalThis.AssertionError = AssertionError;
191 
192declare class OptionalFeatureUnsupportedError extends AssertionError {}
193function OptionalFeatureUnsupportedError(
194 this: OptionalFeatureUnsupportedError,
195 message: string
196): void {
197 AssertionError.call(this, message);
198}
199 
200// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- eslint doesn't like "old-style" classes. Code is copied from WPT
201OptionalFeatureUnsupportedError.prototype = Object.create(
202 AssertionError.prototype
203);
204 
205globalThis.assert_equals = (a, b, message): void => {
206 strictEqual(a, b, message);
207};
208 
209globalThis.assert_not_equals = (a, b, message): void => {
210 notStrictEqual(a, b, message);
211};
212 
213globalThis.assert_true = (val, message): void => {
214 strictEqual(val, true, message);
215};
216 
217globalThis.assert_false = (val, message): void => {
218 strictEqual(val, false, message);
219};
220 
221/**
222 * Assert that ``actual`` and ``expected`` are both arrays, and that the array properties of
223 * ``actual`` and ``expected`` are all the same value (as for :js:func:`assert_equals`).
224 *
225 * @param actual - Test array.
226 * @param expected - Array that is expected to contain the same values as ``actual``.
227 * @param [description] - Description of the condition being tested.
228 */
229globalThis.assert_array_equals = (actual, expected, description): void => {
230 strictEqual(actual.length, expected.length, description);
231 
232 for (let i = 0; i < actual.length; i++) {
233 strictEqual(
234 Object.prototype.hasOwnProperty.call(actual, i),
235 Object.prototype.hasOwnProperty.call(expected, i),
236 description
237 );
238 strictEqual(actual[i], expected[i], description);
239 }
240};
241 
242globalThis.assert_object_equals = (a, b, message): void => {
243 deepStrictEqual(a, b, message);
244};
245 
246/**
247 * Assert that a feature is implemented, based on a 'truthy' condition.
248 *
249 * This function should be used to early-exit from tests in which there is
250 * no point continuing without support for a non-optional spec or spec
251 * feature. For example:
252 *
253 * assert_implements(window.Foo, 'Foo is not supported');
254 *
255 * @param condition The truthy value to test
256 * @param [description] Error description for the case that the condition is not truthy.
257 */
258globalThis.assert_implements = (condition, description): void => {
259 ok(!!condition, description);
260};
261 
262/**
263 * Assert that an optional feature is implemented, based on a 'truthy' condition.
264 *
265 * This function should be used to early-exit from tests in which there is
266 * no point continuing without support for an explicitly optional spec or
267 * spec feature. For example:
268 *
269 * assert_implements_optional(video.canPlayType("video/webm"),
270 * "webm video playback not supported");
271 *
272 * @param condition The truthy value to test
273 * @param [description] Error description for the case that the condition is not truthy.
274 */
275globalThis.assert_implements_optional = (condition, description): void => {
276 if (!condition) {
277 throw new OptionalFeatureUnsupportedError(description ?? '');
278 }
279};
280 
281/**
282 * Asserts if called. Used to ensure that a specific code path is
283 * not taken e.g. that an error event isn't fired.
284 *
285 * @param [description] - Description of the condition being tested.
286 */
287globalThis.assert_unreached = (description): void => {
288 ok(false, `Reached unreachable code: ${description ?? 'undefined'}`);
289};
290 
291/**
292 * Assert a JS Error with the expected constructor is thrown.
293 *
294 * @param constructor The expected exception constructor.
295 * @param func Function which should throw.
296 * @param [description] Error description for the case that the error is not thrown.
297 */
298globalThis.assert_throws_js = (constructor, func, description): void => {
299 throws(
300 () => {
301 func.call(this);
302 },
303 constructor,
304 description
305 );
306};
307 
308/**
309 * Assert the provided value is thrown.
310 *
311 * @param exception The expected exception.
312 * @param fn Function which should throw.
313 * @param [description] Error description for the case that the error is not thrown.
314 */
315globalThis.assert_throws_exactly = (exception, fn, description): void => {
316 try {
317 fn.call(this);
318 } catch (err) {
319 strictEqual(
320 err,
321 exception,
322 description ?? "Thrown exception doesn't match expected value"
323 );
324 return;
325 }
326 
327 fail(description ?? 'No exception was thrown');
328};
329 
330// Maps legacy DOMException code names (e.g. "INVALID_ACCESS_ERR") to modern names
331// (e.g. "InvalidAccessError"). Matches the upstream WPT testharness.js.
332const codename_name_map: Record<string, string> = {
333 INDEX_SIZE_ERR: 'IndexSizeError',
334 HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
335 WRONG_DOCUMENT_ERR: 'WrongDocumentError',
336 INVALID_CHARACTER_ERR: 'InvalidCharacterError',
337 NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
338 NOT_FOUND_ERR: 'NotFoundError',
339 NOT_SUPPORTED_ERR: 'NotSupportedError',
340 INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
341 INVALID_STATE_ERR: 'InvalidStateError',
342 SYNTAX_ERR: 'SyntaxError',
343 INVALID_MODIFICATION_ERR: 'InvalidModificationError',
344 NAMESPACE_ERR: 'NamespaceError',
345 INVALID_ACCESS_ERR: 'InvalidAccessError',
346 TYPE_MISMATCH_ERR: 'TypeMismatchError',
347 SECURITY_ERR: 'SecurityError',
348 NETWORK_ERR: 'NetworkError',
349 ABORT_ERR: 'AbortError',
350 URL_MISMATCH_ERR: 'URLMismatchError',
351 QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
352 TIMEOUT_ERR: 'TimeoutError',
353 INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
354 DATA_CLONE_ERR: 'DataCloneError',
355};
356 
357// Maps modern DOMException names to their legacy numeric codes.
358const name_code_map: Record<string, number> = {
359 IndexSizeError: 1,
360 HierarchyRequestError: 3,
361 WrongDocumentError: 4,
362 InvalidCharacterError: 5,
363 NoModificationAllowedError: 7,
364 NotFoundError: 8,
365 NotSupportedError: 9,
366 InUseAttributeError: 10,
367 InvalidStateError: 11,
368 SyntaxError: 12,
369 InvalidModificationError: 13,
370 NamespaceError: 14,
371 InvalidAccessError: 15,
372 TypeMismatchError: 17,
373 SecurityError: 18,
374 NetworkError: 19,
375 AbortError: 20,
376 URLMismatchError: 21,
377 QuotaExceededError: 22,
378 TimeoutError: 23,
379 InvalidNodeTypeError: 24,
380 DataCloneError: 25,
381 // Modern exceptions with code 0
382 EncodingError: 0,
383 NotReadableError: 0,
384 UnknownError: 0,
385 ConstraintError: 0,
386 DataError: 0,
387 TransactionInactiveError: 0,
388 ReadOnlyError: 0,
389 VersionError: 0,
390 OperationError: 0,
391 NotAllowedError: 0,
392 OptOutError: 0,
393};
394 
395const code_name_map: Record<number, string> = {};
396for (const key in name_code_map) {
397 if ((name_code_map[key] as number) > 0) {
398 code_name_map[name_code_map[key] as number] = key;
399 }
400}
401 
402// Helper for assert_throws_dom_impl assertions. Throws a WPT AssertionError
403// (not node:assert's AssertionError) so instanceof checks work correctly.
404function assert_dom(
405 condition: boolean,
406 assertion_type: string,
407 description: string | undefined,
408 message: string
409): void {
410 if (condition) return;
411 const prefix = description ? `${description}: ` : '';
412 throw new AssertionError(`${assertion_type}: ${prefix}${message}`);
413}
414 
415/**
416 * Internal implementation of assert_throws_dom, matching the upstream WPT
417 * testharness.js assert_throws_dom_impl. Uses its own try/catch rather than
418 * node:assert.throws() so that legacy DOMException code names are mapped to
419 * modern names and error properties are checked individually with clear
420 * messages.
421 */
422function assert_throws_dom_impl(
423 type: number | string,
424 func: ThrowingFn,
425 description: string,
426 assertion_type: string,
427 constructor: typeof DOMException
428): void {
429 try {
430 func();
431 assert_dom(false, assertion_type, description, 'function did not throw');
432 } catch (e) {
433 if (e instanceof AssertionError) {
434 throw e;
435 }
436 
437 // Basic sanity-checks on the thrown exception.
438 assert_dom(
439 typeof e === 'object',
440 assertion_type,
441 description,
442 `thrown value is not an object (got ${typeof e})`
443 );
444 assert_dom(e !== null, assertion_type, description, 'thrown value is null');
445 assert_dom(
446 typeof type === 'number' || typeof type === 'string',
447 assertion_type,
448 description,
449 'type is not a number or string'
450 );
451 
452 const required_props: Record<string, unknown> = {};
453 let name: string;
454 
455 if (typeof type === 'number') {
456 assert_dom(
457 type in code_name_map,
458 assertion_type,
459 description,
460 `Test bug: unrecognized DOMException code "${type}" passed to ${assertion_type}()`
461 );
462 name = code_name_map[type] as string;
463 required_props['code'] = type;
464 } else {
465 // Map legacy code names (e.g. "INVALID_ACCESS_ERR") to modern names
466 name =
467 type in codename_name_map ? (codename_name_map[type] as string) : type;
468 assert_dom(
469 name in name_code_map,
470 assertion_type,
471 description,
472 `Test bug: unrecognized DOMException code name or name "${type}" passed to ${assertion_type}()`
473 );
474 required_props['code'] = name_code_map[name];
475 }
476 
477 if (
478 required_props['code'] === 0 ||
479 ('name' in (e as object) &&
480 (e as { name: string }).name !==
481 (e as { name: string }).name.toUpperCase() &&
482 (e as { name: string }).name !== 'DOMException')
483 ) {
484 // New style exception: also test the name property.
485 required_props['name'] = name;
486 }
487 
488 for (const prop in required_props) {
489 assert_dom(
490 prop in (e as object) &&
491 (e as Record<string, unknown>)[prop] == required_props[prop],
492 assertion_type,
493 description,
494 `thrown exception is not a DOMException ${type}: property ${prop} is equal to ${(e as Record<string, unknown>)[prop]}, expected ${required_props[prop]}`
495 );
496 }
497 
498 // Check that the exception is from the right global. This check is last
499 // so more specific, and more informative, checks on the properties can
500 // happen in case a totally incorrect exception is thrown.
501 assert_dom(
502 (e as object).constructor === constructor,
503 assertion_type,
504 description,
505 'thrown exception from the wrong global'
506 );
507 }
508}
509 
510/**
511 * Assert a DOMException with the expected type is thrown.
512 *
513 * There are two ways of calling assert_throws_dom:
514 *
515 * 1) If the DOMException is expected to come from the current global, the
516 * second argument should be the function expected to throw and a third,
517 * optional, argument is the assertion description.
518 *
519 * 2) If the DOMException is expected to come from some other global, the
520 * second argument should be the DOMException constructor from that global,
521 * the third argument the function expected to throw, and the fourth, optional,
522 * argument the assertion description.
523 *
524 * @param type - The expected exception name or
525 * code. See the `table of names and codes
526 * <https://webidl.spec.whatwg.org/#dfn-error-names-table>`_. If a
527 * number is passed it should be one of the numeric code values in
528 * that table (e.g. 3, 4, etc). If a string is passed it can
529 * either be an exception name (e.g. "HierarchyRequestError",
530 * "WrongDocumentError") or the name of the corresponding error
531 * code (e.g. "``HIERARCHY_REQUEST_ERR``", "``WRONG_DOCUMENT_ERR``").
532 * @param descriptionOrFunc - The function expected to
533 * throw (if the exception comes from another global), or the
534 * optional description of the condition being tested (if the
535 * exception comes from the current global).
536 * @param [maybeDescription] - Description of the condition
537 * being tested (if the exception comes from another global).
538 *
539 */
540globalThis.assert_throws_dom = (
541 type,
542 funcOrConstructor,
543 descriptionOrFunc,
544 maybeDescription
545): void => {
546 let constructor: typeof DOMException;
547 let func: ThrowingFn;
548 let description: string;
549 
550 if (funcOrConstructor.name === 'DOMException') {
551 constructor = funcOrConstructor as typeof DOMException;
552 func = descriptionOrFunc as ThrowingFn;
553 description = maybeDescription as string;
554 } else {
555 constructor = DOMException;
556 func = funcOrConstructor as ThrowingFn;
557 description = descriptionOrFunc as string;
558 ok(
559 maybeDescription === undefined,
560 'Too many args passed to no-constructor version of assert_throws_dom'
561 );
562 }
563 
564 assert_throws_dom_impl(
565 type,
566 func,
567 description,
568 'assert_throws_dom',
569 constructor
570 );
571};
572 
573/**
574 * Assert a QuotaExceededError DOMException is thrown.
575 *
576 * This is a convenience wrapper around assert_throws_dom for the common case
577 * of checking for QuotaExceededError.
578 *
579 * @param func - Function which should throw.
580 * @param [description] - Description of the condition being tested.
581 */
582globalThis.assert_throws_quotaexceedederror = (func, description): void => {
583 assert_throws_dom('QuotaExceededError', func, description ?? '');
584};
585 
586/**
587 * Assert that a Promise is rejected with the right DOMException.
588 *
589 * For the remaining arguments, there are two ways of calling
590 * promise_rejects_dom:
591 *
592 * 1) If the DOMException is expected to come from the current global, the
593 * third argument should be the promise expected to reject, and a fourth,
594 * optional, argument is the assertion description.
595 *
596 * 2) If the DOMException is expected to come from some other global, the
597 * third argument should be the DOMException constructor from that global,
598 * the fourth argument the promise expected to reject, and the fifth,
599 * optional, argument the assertion description.
600 *
601 * @param _test - the `Test` to use for the assertion.
602 * @param type - See documentation for
603 * `assert_throws_dom <#assert_throws_dom>`_.
604 * @param promiseOrConstructor - Either the constructor
605 * for the expected exception (if the exception comes from another
606 * global), or the promise that's expected to reject (if the
607 * exception comes from the current global).
608 * @param descriptionOrPromise - Either the
609 * promise that's expected to reject (if the exception comes from
610 * another global), or the optional description of the condition
611 * being tested (if the exception comes from the current global).
612 * @param [maybeDescription] - Description of the condition
613 * being tested (if the exception comes from another global).
614 *
615 */
616globalThis.promise_rejects_dom = (
617 _test,
618 type,
619 promiseOrConstructor,
620 descriptionOrPromise,
621 maybeDescription
622): Promise<unknown> => {
623 let constructor, promise, description;
624 if (
625 typeof promiseOrConstructor === 'function' &&
626 promiseOrConstructor.name === 'DOMException'
627 ) {
628 constructor = promiseOrConstructor;
629 promise = descriptionOrPromise as Promise<unknown>;
630 description = maybeDescription as string;
631 } else {
632 constructor = DOMException;
633 promise = promiseOrConstructor as Promise<unknown>;
634 description = descriptionOrPromise as string;
635 strictEqual(
636 maybeDescription,
637 undefined,
638 'Too many args passed to no-constructor version of promise_rejects_dom, or accidentally explicitly passed undefined'
639 );
640 }
641 
642 return promise
643 .then(() => {
644 assert_unreached('Should have rejected: ' + description);
645 })
646 .catch(function (e: unknown) {
647 assert_throws_dom(
648 type,
649 constructor,
650 function () {
651 throw e;
652 },
653 description
654 );
655 });
656};
657 
658/**
659 * Assert that ``object`` has an own property with name ``property_name``.
660 *
661 * @param object - Object that should have the given property.
662 * @param property_name - Property name to test.
663 * @param [description] - Description of the condition being tested.
664 */
665globalThis.assert_own_property = (object, property_name, description): void => {
666 ok(
667 Object.prototype.hasOwnProperty.call(object, property_name),
668 `expected property ${String(property_name)} missing on object: ` +
669 (description ?? '')
670 );
671};
672 
673/**
674 * Assert that ``object`` does not have an own property with name ``property_name``.
675 *
676 * @param object - Object that should not have the given property.
677 * @param property_name - Property name to test.
678 * @param [description] - Description of the condition being tested.
679 */
680globalThis.assert_not_own_property = (
681 object,
682 property_name,
683 description
684): void => {
685 ok(
686 !Object.prototype.hasOwnProperty.call(object, property_name),
687 `unexpected property ${String(property_name)} is found on object: ` +
688 (description ?? '')
689 );
690};
691 
692/**
693 * Assert that a Promise is rejected with the right ECMAScript exception.
694 *
695 * @param test - the `Test` to use for the assertion.
696 * @param constructor - The expected exception constructor.
697 * @param promise - The promise that's expected to
698 * reject with the given exception.
699 * @param [description] Error message to add to assert in case of
700 * failure.
701 */
702globalThis.promise_rejects_js = async (
703 _test,
704 constructor,
705 promise,
706 description
707): Promise<void> => {
708 return rejects(promise, constructor, description);
709};
710 
711/**
712 * Assert that ``actual`` matches the RegExp ``expected``.
713 *
714 * @param actual - Test string.
715 * @param expected - RegExp ``actual`` must match.
716 * @param [description] - Description of the condition being tested.
717 */
718globalThis.assert_regexp_match = (actual, expected, description): void => {
719 match(actual, expected, description);
720};
721 
722/**
723 * Assert that ``actual`` is a number greater than ``expected``.
724 *
725 * @param actual - Test value.
726 * @param expected - Number that ``actual`` must be greater than.
727 * @param [description] - Description of the condition being tested.
728 */
729globalThis.assert_greater_than = (actual, expected, description): void => {
730 ok(actual > expected, description);
731};
732 
733/**
734 * Assert that ``actual`` is a number greater than or equal to ``expected``.
735 *
736 * @param actual - Test value.
737 * @param expected - Number that ``actual`` must be greater than or equal to.
738 * @param [description] - Description of the condition being tested.
739 */
740globalThis.assert_greater_than_equal = (
741 actual,
742 expected,
743 description
744): void => {
745 ok(actual >= expected, description);
746};
747 
748/**
749 * Assert that ``actual`` is a number less than ``expected``.
750 *
751 * @param actual - Test value.
752 * @param expected - Number that ``actual`` must be less than.
753 * @param [description] - Description of the condition being tested.
754 */
755globalThis.assert_less_than = (actual, expected, description): void => {
756 ok(actual < expected, description);
757};
758 
759/**
760 * Assert that ``actual`` is a number less than or equal to ``expected``.
761 *
762 * @param actual - Test value.
763 * @param expected - Number that ``actual`` must be less than or equal to.
764 * @param [description] - Description of the condition being tested.
765 */
766globalThis.assert_less_than_equal = (actual, expected, description): void => {
767 ok(actual <= expected, description);
768};
769 
770/**
771 * Assert that a Promise is rejected with the provided value.
772 *
773 * @param test - the `Test` to use for the assertion.
774 * @param exception - The expected value of the rejected promise.
775 * @param promise - The promise that's expected to
776 * reject.
777 * @param [description] Error message to add to assert in case of
778 * failure.
779 */
780globalThis.promise_rejects_exactly = (
781 _test,
782 exception,
783 promise,
784 description
785): Promise<void> => {
786 return promise
787 .then(() => {
788 assert_unreached(`Should have rejected: ${description}`);
789 })
790 .catch((exc: unknown) => {
791 assert_throws_exactly(
792 exception,
793 () => {
794 throw exc;
795 },
796 description
797 );
798 });
799};
800 
801/**
802 * Assert that ``expected`` is an array and ``actual`` is one of the members.
803 * This is implemented using ``indexOf``, so doesn't handle NaN or ±0 correctly.
804 *
805 * @param actual - Test value.
806 * @param expected - An array that ``actual`` is expected to
807 * be a member of.
808 * @param [description] - Description of the condition being tested.
809 */
810globalThis.assert_in_array = (actual, expected, description): void => {
811 notStrictEqual(
812 expected.indexOf(actual),
813 -1,
814 `assert_in_array ${description}: value ${actual} not in array ${expected}`
815 );
816};
817 
818/**
819 * Assert that ``object``'s class string (from ``Object.prototype.toString``)
820 * matches the expected value.
821 *
822 * @param object - The object to check.
823 * @param class_string - Expected class string (without the ``[object ...]`` wrapper).
824 * @param [description] - Description of the condition being tested.
825 */
826globalThis.assert_class_string = (object, class_string, description): void => {
827 const actual = {}.toString.call(object);
828 const expected = `[object ${class_string}]`;
829 strictEqual(actual, expected, description);
830};
831 
832/**
833 * Assert that ``object`` does not have an own property named ``property_name``
834 * but that ``property_name`` is accessible through the prototype chain.
835 *
836 * @param object - Object to test.
837 * @param property_name - Property that should be inherited.
838 * @param [description] - Description of the condition being tested.
839 */
840globalThis.assert_inherits = (object, property_name, description): void => {
841 ok(
842 (typeof object === 'object' && object !== null) ||
843 typeof object === 'function',
844 `assert_inherits: ${description ?? ''}: provided value is not an object`
845 );
846 
847 ok(
848 'hasOwnProperty' in (object as object),
849 `assert_inherits: ${description ?? ''}: provided value has no hasOwnProperty method`
850 );
851 
852 ok(
853 !Object.prototype.hasOwnProperty.call(object, property_name),
854 `assert_inherits: ${description ?? ''}: property ${property_name} found on object, expected in prototype chain`
855 );
856 
857 ok(
858 property_name in (object as object),
859 `assert_inherits: ${description ?? ''}: property ${property_name} not found in prototype chain`
860 );
861};
862 
863/**
864 * Alias for ``assert_inherits``. Asserts that the given property is an IDL
865 * attribute (i.e., is inherited via the prototype chain, not an own property).
866 */
867globalThis.assert_idl_attribute = (
868 object: unknown,
869 property_name: string,
870 description?: string
871): void => {
872 assert_inherits(object, property_name, description);
873};
874 
875/**
876 * Assert that ``object`` has a property ``property_name`` that is read-only
877 * according to its property descriptor. Walks the prototype chain to find
878 * the descriptor, matching upstream WPT testharness.js behaviour.
879 *
880 * @param object - Object to test.
881 * @param property_name - Name of the property that should be read-only.
882 * @param [description] - Description of the condition being tested.
883 */
884globalThis.assert_readonly = (object, property_name, description): void => {
885 ok(
886 property_name in object,
887 `assert_readonly: ${description ?? ''}: property ${property_name} not found`
888 );
889 
890 // Walk the prototype chain, same as upstream testharness.js.
891 let desc: PropertyDescriptor | undefined;
892 let current: object | null = object;
893 while (
894 current !== null &&
895 (desc = Object.getOwnPropertyDescriptor(current, property_name)) ===
896 undefined
897 ) {
898 current = Object.getPrototypeOf(current) as object | null;
899 }
900 
901 ok(
902 desc !== undefined,
903 `assert_readonly: ${description ?? ''}: could not find a descriptor for property ${property_name}`
904 );
905 if ('value' in desc) {
906 strictEqual(
907 desc.writable,
908 false,
909 `assert_readonly: ${description ?? ''}: descriptor [[Writable]] expected false got ${desc.writable}`
910 );
911 } else if ('get' in desc || 'set' in desc) {
912 ok(
913 desc.set === undefined,
914 `assert_readonly: ${description ?? ''}: property ${property_name} is an accessor property with a [[Set]] attribute`
915 );
916 } else {
917 fail(
918 `assert_readonly: ${description ?? ''}: Object.getOwnPropertyDescriptor must return a fully populated property descriptor`
919 );
920 }
921};