mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 12:20:27 +01:00
assert: make partialDeepStrictEqual work with ArrayBuffers
Fixes: https://github.com/nodejs/node/issues/56097 PR-URL: https://github.com/nodejs/node/pull/56098 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
parent
4f51d461d1
commit
dbfcbe371c
255
lib/assert.js
255
lib/assert.js
|
|
@ -21,35 +21,44 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayBufferIsView,
|
||||
ArrayBufferPrototypeGetByteLength,
|
||||
ArrayFrom,
|
||||
ArrayIsArray,
|
||||
ArrayPrototypeIndexOf,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeSlice,
|
||||
DataViewPrototypeGetBuffer,
|
||||
DataViewPrototypeGetByteLength,
|
||||
DataViewPrototypeGetByteOffset,
|
||||
Error,
|
||||
FunctionPrototypeCall,
|
||||
MapPrototypeDelete,
|
||||
MapPrototypeGet,
|
||||
MapPrototypeGetSize,
|
||||
MapPrototypeHas,
|
||||
MapPrototypeSet,
|
||||
NumberIsNaN,
|
||||
ObjectAssign,
|
||||
ObjectIs,
|
||||
ObjectKeys,
|
||||
ObjectPrototypeIsPrototypeOf,
|
||||
ObjectPrototypeToString,
|
||||
ReflectApply,
|
||||
ReflectHas,
|
||||
ReflectOwnKeys,
|
||||
RegExpPrototypeExec,
|
||||
SafeArrayIterator,
|
||||
SafeMap,
|
||||
SafeSet,
|
||||
SafeWeakSet,
|
||||
SetPrototypeGetSize,
|
||||
String,
|
||||
StringPrototypeIndexOf,
|
||||
StringPrototypeSlice,
|
||||
StringPrototypeSplit,
|
||||
SymbolIterator,
|
||||
TypedArrayPrototypeGetLength,
|
||||
Uint8Array,
|
||||
} = primordials;
|
||||
|
||||
const {
|
||||
|
|
@ -65,6 +74,8 @@ const AssertionError = require('internal/assert/assertion_error');
|
|||
const { inspect } = require('internal/util/inspect');
|
||||
const { Buffer } = require('buffer');
|
||||
const {
|
||||
isArrayBuffer,
|
||||
isDataView,
|
||||
isKeyObject,
|
||||
isPromise,
|
||||
isRegExp,
|
||||
|
|
@ -73,6 +84,8 @@ const {
|
|||
isDate,
|
||||
isWeakSet,
|
||||
isWeakMap,
|
||||
isSharedArrayBuffer,
|
||||
isAnyArrayBuffer,
|
||||
} = require('internal/util/types');
|
||||
const { isError, deprecate, emitExperimentalWarning } = require('internal/util');
|
||||
const { innerOk } = require('internal/assert/utils');
|
||||
|
|
@ -369,9 +382,161 @@ function isSpecial(obj) {
|
|||
}
|
||||
|
||||
const typesToCallDeepStrictEqualWith = [
|
||||
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer,
|
||||
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer, isSharedArrayBuffer,
|
||||
];
|
||||
|
||||
function compareMaps(actual, expected, comparedObjects) {
|
||||
if (MapPrototypeGetSize(actual) !== MapPrototypeGetSize(expected)) {
|
||||
return false;
|
||||
}
|
||||
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
|
||||
|
||||
comparedObjects ??= new SafeWeakSet();
|
||||
|
||||
for (const { 0: key, 1: val } of safeIterator) {
|
||||
if (!MapPrototypeHas(expected, key)) {
|
||||
return false;
|
||||
}
|
||||
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function partiallyCompareArrayBuffersOrViews(actual, expected) {
|
||||
let actualView, expectedView, expectedViewLength;
|
||||
|
||||
if (!ArrayBufferIsView(actual)) {
|
||||
let actualViewLength;
|
||||
|
||||
if (isArrayBuffer(actual) && isArrayBuffer(expected)) {
|
||||
actualViewLength = ArrayBufferPrototypeGetByteLength(actual);
|
||||
expectedViewLength = ArrayBufferPrototypeGetByteLength(expected);
|
||||
} else if (isSharedArrayBuffer(actual) && isSharedArrayBuffer(expected)) {
|
||||
actualViewLength = actual.byteLength;
|
||||
expectedViewLength = expected.byteLength;
|
||||
} else {
|
||||
// Cannot compare ArrayBuffers with SharedArrayBuffers
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedViewLength > actualViewLength) {
|
||||
return false;
|
||||
}
|
||||
actualView = new Uint8Array(actual);
|
||||
expectedView = new Uint8Array(expected);
|
||||
|
||||
} else if (isDataView(actual)) {
|
||||
if (!isDataView(expected)) {
|
||||
return false;
|
||||
}
|
||||
const actualByteLength = DataViewPrototypeGetByteLength(actual);
|
||||
expectedViewLength = DataViewPrototypeGetByteLength(expected);
|
||||
if (expectedViewLength > actualByteLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
actualView = new Uint8Array(
|
||||
DataViewPrototypeGetBuffer(actual),
|
||||
DataViewPrototypeGetByteOffset(actual),
|
||||
actualByteLength,
|
||||
);
|
||||
expectedView = new Uint8Array(
|
||||
DataViewPrototypeGetBuffer(expected),
|
||||
DataViewPrototypeGetByteOffset(expected),
|
||||
expectedViewLength,
|
||||
);
|
||||
} else {
|
||||
if (ObjectPrototypeToString(actual) !== ObjectPrototypeToString(expected)) {
|
||||
return false;
|
||||
}
|
||||
actualView = actual;
|
||||
expectedView = expected;
|
||||
expectedViewLength = TypedArrayPrototypeGetLength(expected);
|
||||
|
||||
if (expectedViewLength > TypedArrayPrototypeGetLength(actual)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < expectedViewLength; i++) {
|
||||
if (actualView[i] !== expectedView[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function partiallyCompareSets(actual, expected, comparedObjects) {
|
||||
if (SetPrototypeGetSize(expected) > SetPrototypeGetSize(actual)) {
|
||||
return false; // `expected` can't be a subset if it has more elements
|
||||
}
|
||||
|
||||
if (isDeepEqual === undefined) lazyLoadComparison();
|
||||
|
||||
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
|
||||
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
|
||||
const usedIndices = new SafeSet();
|
||||
|
||||
expectedIteration: for (const expectedItem of expectedIterator) {
|
||||
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
|
||||
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
|
||||
usedIndices.add(actualIdx);
|
||||
continue expectedIteration;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function partiallyCompareArrays(actual, expected, comparedObjects) {
|
||||
if (expected.length > actual.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isDeepEqual === undefined) lazyLoadComparison();
|
||||
|
||||
// Create a map to count occurrences of each element in the expected array
|
||||
const expectedCounts = new SafeMap();
|
||||
for (const expectedItem of expected) {
|
||||
let found = false;
|
||||
for (const { 0: key, 1: count } of expectedCounts) {
|
||||
if (isDeepStrictEqual(key, expectedItem)) {
|
||||
expectedCounts.set(key, count + 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
expectedCounts.set(expectedItem, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const safeActual = new SafeArrayIterator(actual);
|
||||
|
||||
// Create a map to count occurrences of relevant elements in the actual array
|
||||
for (const actualItem of safeActual) {
|
||||
for (const { 0: key, 1: count } of expectedCounts) {
|
||||
if (isDeepStrictEqual(key, actualItem)) {
|
||||
if (count === 1) {
|
||||
expectedCounts.delete(key);
|
||||
} else {
|
||||
expectedCounts.set(key, count - 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { size } = expectedCounts;
|
||||
expectedCounts.clear();
|
||||
return size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two objects or values recursively to check if they are equal.
|
||||
* @param {any} actual - The actual value to compare.
|
||||
|
|
@ -388,22 +553,16 @@ function compareBranch(
|
|||
) {
|
||||
// Check for Map object equality
|
||||
if (isMap(actual) && isMap(expected)) {
|
||||
if (actual.size !== expected.size) {
|
||||
return false;
|
||||
}
|
||||
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
|
||||
return compareMaps(actual, expected, comparedObjects);
|
||||
}
|
||||
|
||||
comparedObjects ??= new SafeWeakSet();
|
||||
|
||||
for (const { 0: key, 1: val } of safeIterator) {
|
||||
if (!MapPrototypeHas(expected, key)) {
|
||||
return false;
|
||||
}
|
||||
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
if (
|
||||
ArrayBufferIsView(actual) ||
|
||||
isAnyArrayBuffer(actual) ||
|
||||
ArrayBufferIsView(expected) ||
|
||||
isAnyArrayBuffer(expected)
|
||||
) {
|
||||
return partiallyCompareArrayBuffersOrViews(actual, expected);
|
||||
}
|
||||
|
||||
for (const type of typesToCallDeepStrictEqualWith) {
|
||||
|
|
@ -415,68 +574,12 @@ function compareBranch(
|
|||
|
||||
// Check for Set object equality
|
||||
if (isSet(actual) && isSet(expected)) {
|
||||
if (expected.size > actual.size) {
|
||||
return false; // `expected` can't be a subset if it has more elements
|
||||
}
|
||||
|
||||
if (isDeepEqual === undefined) lazyLoadComparison();
|
||||
|
||||
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
|
||||
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
|
||||
const usedIndices = new SafeSet();
|
||||
|
||||
expectedIteration: for (const expectedItem of expectedIterator) {
|
||||
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
|
||||
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
|
||||
usedIndices.add(actualIdx);
|
||||
continue expectedIteration;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return partiallyCompareSets(actual, expected, comparedObjects);
|
||||
}
|
||||
|
||||
// Check if expected array is a subset of actual array
|
||||
if (ArrayIsArray(actual) && ArrayIsArray(expected)) {
|
||||
if (expected.length > actual.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isDeepEqual === undefined) lazyLoadComparison();
|
||||
|
||||
// Create a map to count occurrences of each element in the expected array
|
||||
const expectedCounts = new SafeMap();
|
||||
for (const expectedItem of expected) {
|
||||
let found = false;
|
||||
for (const { 0: key, 1: count } of expectedCounts) {
|
||||
if (isDeepStrictEqual(key, expectedItem)) {
|
||||
MapPrototypeSet(expectedCounts, key, count + 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
MapPrototypeSet(expectedCounts, expectedItem, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a map to count occurrences of relevant elements in the actual array
|
||||
for (const actualItem of actual) {
|
||||
for (const { 0: key, 1: count } of expectedCounts) {
|
||||
if (isDeepStrictEqual(key, actualItem)) {
|
||||
if (count === 1) {
|
||||
MapPrototypeDelete(expectedCounts, key);
|
||||
} else {
|
||||
MapPrototypeSet(expectedCounts, key, count - 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !expectedCounts.size;
|
||||
return partiallyCompareArrays(actual, expected, comparedObjects);
|
||||
}
|
||||
|
||||
// Comparison done when at least one of the values is not an object
|
||||
|
|
|
|||
|
|
@ -39,10 +39,15 @@ describe('Object Comparison Tests', () => {
|
|||
describe('throws an error', () => {
|
||||
const tests = [
|
||||
{
|
||||
description: 'throws when only one argument is provided',
|
||||
description: 'throws when only actual is provided',
|
||||
actual: { a: 1 },
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
description: 'throws when only expected is provided',
|
||||
actual: undefined,
|
||||
expected: { a: 1 },
|
||||
},
|
||||
{
|
||||
description: 'throws when expected has more properties than actual',
|
||||
actual: [1, 'two'],
|
||||
|
|
@ -207,6 +212,74 @@ describe('Object Comparison Tests', () => {
|
|||
actual: [1, 2, 3],
|
||||
expected: ['2'],
|
||||
},
|
||||
{
|
||||
description: 'throws when comparing an ArrayBuffer with a Uint8Array',
|
||||
actual: new ArrayBuffer(3),
|
||||
expected: new Uint8Array(3),
|
||||
},
|
||||
{
|
||||
description: 'throws when comparing a ArrayBuffer with a SharedArrayBuffer',
|
||||
actual: new ArrayBuffer(3),
|
||||
expected: new SharedArrayBuffer(3),
|
||||
},
|
||||
{
|
||||
description: 'throws when comparing a SharedArrayBuffer with an ArrayBuffer',
|
||||
actual: new SharedArrayBuffer(3),
|
||||
expected: new ArrayBuffer(3),
|
||||
},
|
||||
{
|
||||
description: 'throws when comparing an Int16Array with a Uint16Array',
|
||||
actual: new Int16Array(3),
|
||||
expected: new Uint16Array(3),
|
||||
},
|
||||
{
|
||||
description: 'throws when comparing two dataviews with different buffers',
|
||||
actual: { dataView: new DataView(new ArrayBuffer(3)) },
|
||||
expected: { dataView: new DataView(new ArrayBuffer(4)) },
|
||||
},
|
||||
{
|
||||
description: 'throws because expected Uint8Array(SharedArrayBuffer) is not a subset of actual',
|
||||
actual: { typedArray: new Uint8Array(new SharedArrayBuffer(3)) },
|
||||
expected: { typedArray: new Uint8Array(new SharedArrayBuffer(5)) },
|
||||
},
|
||||
{
|
||||
description: 'throws because expected SharedArrayBuffer is not a subset of actual',
|
||||
actual: { typedArray: new SharedArrayBuffer(3) },
|
||||
expected: { typedArray: new SharedArrayBuffer(5) },
|
||||
},
|
||||
{
|
||||
description: 'throws when comparing a DataView with a TypedArray',
|
||||
actual: { dataView: new DataView(new ArrayBuffer(3)) },
|
||||
expected: { dataView: new Uint8Array(3) },
|
||||
},
|
||||
{
|
||||
description: 'throws when comparing a TypedArray with a DataView',
|
||||
actual: { dataView: new Uint8Array(3) },
|
||||
expected: { dataView: new DataView(new ArrayBuffer(3)) },
|
||||
},
|
||||
{
|
||||
description: 'throws when comparing SharedArrayBuffers when expected has different elements actual',
|
||||
actual: (() => {
|
||||
const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
|
||||
const sharedArray = new Int32Array(sharedBuffer);
|
||||
|
||||
sharedArray[0] = 1;
|
||||
sharedArray[1] = 2;
|
||||
sharedArray[2] = 3;
|
||||
|
||||
return sharedBuffer;
|
||||
})(),
|
||||
expected: (() => {
|
||||
const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
|
||||
const sharedArray = new Int32Array(sharedBuffer);
|
||||
|
||||
sharedArray[0] = 1;
|
||||
sharedArray[1] = 2;
|
||||
sharedArray[2] = 6;
|
||||
|
||||
return sharedBuffer;
|
||||
})(),
|
||||
},
|
||||
];
|
||||
|
||||
if (common.hasCrypto) {
|
||||
|
|
@ -343,10 +416,89 @@ describe('Object Comparison Tests', () => {
|
|||
expected: { error: new Error('Test error') },
|
||||
},
|
||||
{
|
||||
description: 'compares two objects with TypedArray instances with the same content',
|
||||
actual: { typedArray: new Uint8Array([1, 2, 3]) },
|
||||
description: 'compares two Uint8Array objects',
|
||||
actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) },
|
||||
expected: { typedArray: new Uint8Array([1, 2, 3]) },
|
||||
},
|
||||
{
|
||||
description: 'compares two Int16Array objects',
|
||||
actual: { typedArray: new Int16Array([1, 2, 3, 4, 5]) },
|
||||
expected: { typedArray: new Int16Array([1, 2, 3]) },
|
||||
},
|
||||
{
|
||||
description: 'compares two DataView objects with the same buffer and different views',
|
||||
actual: { dataView: new DataView(new ArrayBuffer(8), 0, 4) },
|
||||
expected: { dataView: new DataView(new ArrayBuffer(8), 4, 4) },
|
||||
},
|
||||
{
|
||||
description: 'compares two DataView objects with different buffers',
|
||||
actual: { dataView: new DataView(new ArrayBuffer(8)) },
|
||||
expected: { dataView: new DataView(new ArrayBuffer(8)) },
|
||||
},
|
||||
{
|
||||
description: 'compares two DataView objects with the same buffer and same views',
|
||||
actual: { dataView: new DataView(new ArrayBuffer(8), 0, 8) },
|
||||
expected: { dataView: new DataView(new ArrayBuffer(8), 0, 8) },
|
||||
},
|
||||
{
|
||||
description: 'compares two SharedArrayBuffers with the same length',
|
||||
actual: new SharedArrayBuffer(3),
|
||||
expected: new SharedArrayBuffer(3),
|
||||
},
|
||||
{
|
||||
description: 'compares two Uint8Array objects from SharedArrayBuffer',
|
||||
actual: { typedArray: new Uint8Array(new SharedArrayBuffer(5)) },
|
||||
expected: { typedArray: new Uint8Array(new SharedArrayBuffer(3)) },
|
||||
},
|
||||
{
|
||||
description: 'compares two Int16Array objects from SharedArrayBuffer',
|
||||
actual: { typedArray: new Int16Array(new SharedArrayBuffer(10)) },
|
||||
expected: { typedArray: new Int16Array(new SharedArrayBuffer(6)) },
|
||||
},
|
||||
{
|
||||
description: 'compares two DataView objects with the same SharedArrayBuffer and different views',
|
||||
actual: { dataView: new DataView(new SharedArrayBuffer(8), 0, 4) },
|
||||
expected: { dataView: new DataView(new SharedArrayBuffer(8), 4, 4) },
|
||||
},
|
||||
{
|
||||
description: 'compares two DataView objects with different SharedArrayBuffers',
|
||||
actual: { dataView: new DataView(new SharedArrayBuffer(8)) },
|
||||
expected: { dataView: new DataView(new SharedArrayBuffer(8)) },
|
||||
},
|
||||
{
|
||||
description: 'compares two DataView objects with the same SharedArrayBuffer and same views',
|
||||
actual: { dataView: new DataView(new SharedArrayBuffer(8), 0, 8) },
|
||||
expected: { dataView: new DataView(new SharedArrayBuffer(8), 0, 8) },
|
||||
},
|
||||
{
|
||||
description: 'compares two SharedArrayBuffers',
|
||||
actual: { typedArray: new SharedArrayBuffer(5) },
|
||||
expected: { typedArray: new SharedArrayBuffer(3) },
|
||||
},
|
||||
{
|
||||
description: 'compares two SharedArrayBuffers with data inside',
|
||||
actual: (() => {
|
||||
const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
|
||||
const sharedArray = new Int32Array(sharedBuffer);
|
||||
|
||||
sharedArray[0] = 1;
|
||||
sharedArray[1] = 2;
|
||||
sharedArray[2] = 3;
|
||||
sharedArray[3] = 4;
|
||||
|
||||
return sharedBuffer;
|
||||
})(),
|
||||
expected: (() => {
|
||||
const sharedBuffer = new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT);
|
||||
const sharedArray = new Int32Array(sharedBuffer);
|
||||
|
||||
sharedArray[0] = 1;
|
||||
sharedArray[1] = 2;
|
||||
sharedArray[2] = 3;
|
||||
|
||||
return sharedBuffer;
|
||||
})(),
|
||||
},
|
||||
{
|
||||
description: 'compares two Map objects with identical entries',
|
||||
actual: new Map([
|
||||
|
|
@ -358,6 +510,19 @@ describe('Object Comparison Tests', () => {
|
|||
['key2', 'value2'],
|
||||
]),
|
||||
},
|
||||
{
|
||||
description: 'compares two Map where one is a subset of the other',
|
||||
actual: new Map([
|
||||
['key1', { nested: { property: true } }],
|
||||
['key2', new Set([1, 2, 3])],
|
||||
['key3', new Uint8Array([1, 2, 3])],
|
||||
]),
|
||||
expected: new Map([
|
||||
['key1', { nested: { property: true } }],
|
||||
['key2', new Set([1, 2, 3])],
|
||||
['key3', new Uint8Array([1, 2, 3])],
|
||||
])
|
||||
},
|
||||
{
|
||||
describe: 'compares two array of objects',
|
||||
actual: [{ a: 5 }],
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ suite('notEqualArrayPairs', () => {
|
|||
new Uint8Array(new ArrayBuffer(3)).fill(1).buffer,
|
||||
new Uint8Array(new SharedArrayBuffer(3)).fill(2).buffer,
|
||||
],
|
||||
[new ArrayBuffer(3), new SharedArrayBuffer(3)],
|
||||
[new SharedArrayBuffer(2), new ArrayBuffer(2)],
|
||||
];
|
||||
|
||||
for (const arrayPair of notEqualArrayPairs) {
|
||||
|
|
@ -99,6 +101,10 @@ suite('notEqualArrayPairs', () => {
|
|||
makeBlock(assert.deepStrictEqual, arrayPair[0], arrayPair[1]),
|
||||
assert.AssertionError
|
||||
);
|
||||
assert.throws(
|
||||
makeBlock(assert.partialDeepStrictEqual, arrayPair[0], arrayPair[1]),
|
||||
assert.AssertionError
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user