🏡 index : quine-global/webdriverio.git

import { types as nodeUtilTypes } from 'node:util'
import type { AssertionError } from 'node:assert'

import { diffWordsWithSpace } from 'diff'
import objectInspect from 'object-inspect'

import RunnableStats from './runnable.js'
import { pad, color, colorLines } from '../utils.js'
import type { Argument } from '../types.js'

const maxStringLength = 2048

export interface Test {
    type: 'test:start' | 'test:pass' | 'test:fail' | 'test:retry' | 'test:pending' | 'test:end' | 'test:skip'
    title: string
    parent: string
    fullTitle: string
    pending: boolean
    file?: string
    body?: string
    duration?: number
    cid: string
    specs: string[]
    uid: string
    pendingReason?: string
    error?: Error
    errors?: Error[]
    retries?: number
    argument?: string | Argument
    state?: string
}

interface Output {
    command: string
    params: unknown[]
    method: 'PUT' | 'POST' | 'GET' | 'DELETE'
    endpoint: string
    body: {}
    result: {
        value: string | null
    }
    sessionId: string
    cid: string
    type: 'command' | 'result'
}

/**
 * TestStats class
 * captures data on a test.
 */
export default class TestStats extends RunnableStats {
    uid: string
    cid: string
    title: string
    currentTest?: string
    fullTitle: string
    output: Output[]
    argument?: string | Argument
    retries?: number
    parent: string
    /**
     * initial test state is pending
     * the state can change to the following: passed, skipped, failed
     */
    state: 'pending' | 'passed' | 'skipped' | 'failed'
    pendingReason?: string
    errors?: Error[]
    error?: Error
    body?: string

    constructor(test: Test) {
        super('test')
        this.uid = RunnableStats.getIdentifier(test)
        this.cid = test.cid
        this.title = test.title
        this.fullTitle = test.fullTitle
        this.output = []
        this.argument = test.argument
        this.retries = test.retries
        this.parent = test.parent
        // Mocha only
        this.body = test.body

        /**
         * initial test state is pending
         * the state can change to the following: passed, skipped, failed
         */
        this.state = 'pending'
    }

    pass() {
        this.complete()
        this.state = 'passed'
    }

    skip(reason: string) {
        this.pendingReason = reason
        this.state = 'skipped'
    }

    fail(errors?: Error[]) {
        this.complete()
        this.state = 'failed'

        /**
         * Iterates through all errors to check if they're a type of 'AssertionError',
         * and formats it if so. Otherwise, just leaves error as is
         */
        const formattedErrors = errors?.map((err: Error) => (
            /**
             * only format if error object has either an "expected" or "actual" property set
             */
            (((err as AssertionError).expected || (err as AssertionError).actual) && !nodeUtilTypes.isProxy((err as AssertionError).actual)) &&
            /**
             * and if they aren't already formated, e.g. in Jasmine
             */
            (err.message && !err.message.includes('Expected: ') && !err.message.includes('Received: '))
                ? this._stringifyDiffObjs(err as AssertionError)
                : err
        ))

        this.errors = formattedErrors
        if (formattedErrors && formattedErrors.length) {
            this.error = formattedErrors[0]
        }
    }

    private _stringifyDiffObjs (err: AssertionError) {
        const inspectOpts = { maxStringLength }
        const expected = objectInspect(err.expected, inspectOpts)
        const actual = objectInspect(err.actual, inspectOpts)

        let msg = diffWordsWithSpace(actual, expected)
            .map((str) => (
                str.added
                    ? colorLines('diff added inline', str.value)
                    : str.removed
                        ? colorLines('diff removed inline', str.value)
                        : str.value
            ))
            .join('')

        // linenos
        const lines = msg.split('\n')
        if (lines.length > 4) {
            const width = String(lines.length).length
            msg = lines
                .map(function(str: string, i: number) {
                    return pad(String(++i), width) + ' |' + ' ' + str
                })
                .join('\n')
        }

        // legend
        msg = `\n${color('diff removed inline', 'actual')} ${color('diff added inline', 'expected')}\n\n${msg}\n`

        // indent
        msg = msg.replace(/^/gm, '      ')
        const newError = new Error(err.message + msg)
        newError.stack = err.stack
        return newError
    }
}