Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: testing matcher and logging #7363

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 166 additions & 36 deletions packages/rxjs/spec/helpers/observableMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,179 @@
import * as _ from 'lodash';
import * as chai from 'chai';
import { ErrorNotification, NextNotification, ObservableNotification } from 'rxjs';
import { TestMessage } from 'rxjs/internal/testing/TestMessage';
import { SubscriptionLog } from 'rxjs/internal/testing/subscription-logging';

function stringify(x: any): string {
return JSON.stringify(x, function (key: string, value: any) {
if (Array.isArray(value)) {
return '[' + value
.map(function (i) {
return '\n\t' + stringify(i);
}) + '\n]';
}
return value;
})
.replace(/\\"/g, '"')
.replace(/\\t/g, '\t')
.replace(/\\n/g, '\n');
}

function deleteErrorNotificationStack(marble: any) {
const { notification } = marble;
if (notification) {
const { kind, error } = notification;
if (kind === 'E' && error instanceof Error) {
notification.error = { name: error.name, message: error.message };
}
function stringifyValue(obj: any): string {
// Handle null
if (obj === null) {
return 'null';
}

// Check if it's a plain object
if (typeof obj === 'object' && (Array.isArray(obj) || obj.constructor === Object)) {
return JSON.stringify(obj);
}

// If it's an instance of a class (or built-in like Date, RegExp, etc.)
if (typeof obj === 'object' && obj.constructor && obj.constructor.name) {
return `[instanceof ${obj.constructor.name}]`;
}
return marble;

// Just in case there's some edge case not covered, return a generic string representation
return String(obj);
}

function testMessageToString(testMessage: TestMessage, indent: number, frameOffset: number) {
const indentation = ' '.repeat(indent);
const { notification, frame } = testMessage;
const currentFrame = frame + frameOffset;
let result = `\t${indentation}${currentFrame}: `;

switch (notification.kind) {
case 'N':
if (isTestMessageArray(notification.value)) {
result += `$ {\n${indentation}${testMessagesToString(notification.value, indent + 1, currentFrame)}\n\t${indentation}}`;
} else {
result += stringifyValue(notification.value);
}
break;
case 'E':
result += 'ERROR';
if (notification.error?.name) {
result += ` ${notification.error.name}`;
}
if (notification.error?.message) {
result += `: ${notification.error.message}`;
}
break;
case 'C':
result += 'COMPLETE';
break;
}

return result;
}

function testMessagesToString(testMessages: TestMessage[], indent = 0, frameOffset = 0) {
return testMessages.map((testMessage) => testMessageToString(testMessage, indent, frameOffset)).join('\n');
}

export function observableMatcher(actual: any, expected: any) {
if (Array.isArray(actual) && Array.isArray(expected)) {
actual = actual.map(deleteErrorNotificationStack);
expected = expected.map(deleteErrorNotificationStack);
const passed = _.isEqual(actual, expected);
if (passed) {
return;
if (!testMessagesEqual(actual, expected)) {
if (isTestMessageArray(expected)) {
let message = '\n\tExpected \n';
message += testMessagesToString(actual, 1);
message += '\n\tto equal \n';
message += testMessagesToString(expected, 1);

chai.assert(false, message);
} else {
let message = '\n\tExpected \n';
message += '\t\t' + JSON.stringify(actual);
message += '\n\tto equal \n';
message += '\t\t' + JSON.stringify(expected);

chai.assert(false, message);
}
}
}

function testMessagesEqual(expected: SubscriptionLog[] | TestMessage[], actual: SubscriptionLog[] | TestMessage[]) {
if (expected.length !== actual.length) {
// If they're not the same length, we know they're not equal.
return false;
}

if (expected.length === 0) {
// Two empty arrays are always going to be equal.
return true;
}

if (isTestMessageArray(expected)) {
if (!isTestMessageArray(actual)) {
return false;
}

// TestMessages
for (let i = 0; i < expected.length; i++) {
const aMsg = expected[i];
const bMsg = actual[i];
if (aMsg.frame !== bMsg.frame) {
return false;
}
const aNotification = aMsg.notification;
const bNotification = bMsg.notification;

if (aNotification.kind !== bNotification.kind) {
return false;
}
if (aNotification.kind === 'N') {
const aNotificationValue = aNotification.value;
const bNotificationValue = (bNotification as NextNotification<any>).value;

if (isTestMessageArray(aNotificationValue)) {
// We are testing inner observable values.
// That means we'll be matching test messages for that inner observable.
if (!isTestMessageArray(bNotificationValue)) {
return false;
}

if (!testMessagesEqual(aNotificationValue, bNotificationValue)) {
return false;
}
} else {
return _.isEqual(aNotificationValue, bNotificationValue);
}
} else if (aNotification.kind === 'E') {
return errorNotifcationsEqual(aNotification, bNotification as ErrorNotification);
}
}
return true;
}

if (isSubscriptionLogArray(expected)) {
if (!isSubscriptionLogArray(actual)) {
return false;
}

let message = '\nExpected \n';
actual.forEach((x: any) => message += `\t${stringify(x)}\n`);
for (let i = 0; i < expected.length; i++) {
const aLog = expected[i];
const bLog = actual[i];

message += '\t\nto deep equal \n';
expected.forEach((x: any) => message += `\t${stringify(x)}\n`);
if (aLog.subscribedFrame !== bLog.subscribedFrame || aLog.unsubscribedFrame !== bLog.unsubscribedFrame) {
return false;
}
}

chai.assert(passed, message);
} else {
chai.assert.deepEqual(actual, expected);
return true;
}

return false;
}

function errorNotifcationsEqual(a: ErrorNotification, b: ErrorNotification) {
return a.error.name === b.error.name && a.error.message === b.error.message;
}

function isTestMessageArray(input: unknown): input is TestMessage[] {
return isArrayOf<TestMessage>(input, 'frame');
}

function isSubscriptionLogArray(input: unknown): input is SubscriptionLog[] {
return isArrayOf<SubscriptionLog>(input, 'subscribedFrame');
}

function isArrayOf<T>(input: unknown, propName: keyof T): input is T[] {
if (!Array.isArray(input)) {
return false;
}

// An empty array could match any type of array.
if (input.length === 0) {
return true;
}

const first = input[0];
return typeof first === 'object' && first && propName in first;
}
Loading