@plugjs/expect5 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.d.mts +2 -0
  3. package/dist/cli.mjs +96 -0
  4. package/dist/cli.mjs.map +6 -0
  5. package/dist/execution/executable.cjs +299 -0
  6. package/dist/execution/executable.cjs.map +6 -0
  7. package/dist/execution/executable.d.ts +87 -0
  8. package/dist/execution/executable.mjs +260 -0
  9. package/dist/execution/executable.mjs.map +6 -0
  10. package/dist/execution/executor.cjs +125 -0
  11. package/dist/execution/executor.cjs.map +6 -0
  12. package/dist/execution/executor.d.ts +35 -0
  13. package/dist/execution/executor.mjs +90 -0
  14. package/dist/execution/executor.mjs.map +6 -0
  15. package/dist/execution/setup.cjs +127 -0
  16. package/dist/execution/setup.cjs.map +6 -0
  17. package/dist/execution/setup.d.ts +31 -0
  18. package/dist/execution/setup.mjs +87 -0
  19. package/dist/execution/setup.mjs.map +6 -0
  20. package/dist/expectation/basic.cjs +216 -0
  21. package/dist/expectation/basic.cjs.map +6 -0
  22. package/dist/expectation/basic.d.ts +47 -0
  23. package/dist/expectation/basic.mjs +177 -0
  24. package/dist/expectation/basic.mjs.map +6 -0
  25. package/dist/expectation/diff.cjs +253 -0
  26. package/dist/expectation/diff.cjs.map +6 -0
  27. package/dist/expectation/diff.d.ts +27 -0
  28. package/dist/expectation/diff.mjs +228 -0
  29. package/dist/expectation/diff.mjs.map +6 -0
  30. package/dist/expectation/expect.cjs +211 -0
  31. package/dist/expectation/expect.cjs.map +6 -0
  32. package/dist/expectation/expect.d.ts +140 -0
  33. package/dist/expectation/expect.mjs +219 -0
  34. package/dist/expectation/expect.mjs.map +6 -0
  35. package/dist/expectation/include.cjs +187 -0
  36. package/dist/expectation/include.cjs.map +6 -0
  37. package/dist/expectation/include.d.ts +10 -0
  38. package/dist/expectation/include.mjs +158 -0
  39. package/dist/expectation/include.mjs.map +6 -0
  40. package/dist/expectation/print.cjs +281 -0
  41. package/dist/expectation/print.cjs.map +6 -0
  42. package/dist/expectation/print.d.ts +4 -0
  43. package/dist/expectation/print.mjs +256 -0
  44. package/dist/expectation/print.mjs.map +6 -0
  45. package/dist/expectation/throwing.cjs +58 -0
  46. package/dist/expectation/throwing.cjs.map +6 -0
  47. package/dist/expectation/throwing.d.ts +8 -0
  48. package/dist/expectation/throwing.mjs +32 -0
  49. package/dist/expectation/throwing.mjs.map +6 -0
  50. package/dist/expectation/types.cjs +212 -0
  51. package/dist/expectation/types.cjs.map +6 -0
  52. package/dist/expectation/types.d.ts +57 -0
  53. package/dist/expectation/types.mjs +178 -0
  54. package/dist/expectation/types.mjs.map +6 -0
  55. package/dist/expectation/void.cjs +111 -0
  56. package/dist/expectation/void.cjs.map +6 -0
  57. package/dist/expectation/void.d.ts +39 -0
  58. package/dist/expectation/void.mjs +77 -0
  59. package/dist/expectation/void.mjs.map +6 -0
  60. package/dist/globals.cjs +2 -0
  61. package/dist/globals.cjs.map +6 -0
  62. package/dist/globals.d.ts +23 -0
  63. package/dist/globals.mjs +1 -0
  64. package/dist/globals.mjs.map +6 -0
  65. package/dist/index.cjs +66 -0
  66. package/dist/index.cjs.map +6 -0
  67. package/dist/index.d.ts +29 -0
  68. package/dist/index.mjs +41 -0
  69. package/dist/index.mjs.map +6 -0
  70. package/dist/test.cjs +229 -0
  71. package/dist/test.cjs.map +6 -0
  72. package/dist/test.d.ts +9 -0
  73. package/dist/test.mjs +194 -0
  74. package/dist/test.mjs.map +6 -0
  75. package/package.json +57 -0
  76. package/src/cli.mts +122 -0
  77. package/src/execution/executable.ts +364 -0
  78. package/src/execution/executor.ts +146 -0
  79. package/src/execution/setup.ts +108 -0
  80. package/src/expectation/basic.ts +209 -0
  81. package/src/expectation/diff.ts +445 -0
  82. package/src/expectation/expect.ts +401 -0
  83. package/src/expectation/include.ts +184 -0
  84. package/src/expectation/print.ts +386 -0
  85. package/src/expectation/throwing.ts +45 -0
  86. package/src/expectation/types.ts +263 -0
  87. package/src/expectation/void.ts +80 -0
  88. package/src/globals.ts +30 -0
  89. package/src/index.ts +54 -0
  90. package/src/test.ts +239 -0
@@ -0,0 +1,80 @@
1
+ import { ExpectationError, stringifyValue } from './types'
2
+
3
+ import type { Expectation, Expectations } from './expect'
4
+
5
+ /** A simple {@link Expectation} performing a basic true/false check */
6
+ abstract class VoidExpectation implements Expectation {
7
+ constructor(
8
+ private _details: string,
9
+ private _check: (value: unknown) => boolean,
10
+ ) {}
11
+
12
+ expect(context: Expectations, negative: boolean): void {
13
+ const check = this._check(context.value)
14
+ if (check === negative) {
15
+ throw new ExpectationError(context, negative, this._details)
16
+ }
17
+ }
18
+ }
19
+
20
+ /* ========================================================================== */
21
+
22
+ export class ToBeDefined extends VoidExpectation {
23
+ constructor() {
24
+ super('to be defined', (value) => (value !== null) && (value !== undefined))
25
+ }
26
+ }
27
+
28
+ export class ToBeFalse extends VoidExpectation {
29
+ constructor() {
30
+ super(`to be ${stringifyValue(false)}`, (value) => value === false)
31
+ }
32
+ }
33
+
34
+ export class ToBeFalsy extends VoidExpectation {
35
+ constructor() {
36
+ super('to be falsy', (value) => ! value)
37
+ }
38
+ }
39
+
40
+ export class ToBeNaN extends VoidExpectation {
41
+ constructor() {
42
+ super(`to be ${stringifyValue(NaN)}`, (value) => (typeof value === 'number') && isNaN(value))
43
+ }
44
+ }
45
+
46
+ export class ToBeNegativeInfinity extends VoidExpectation {
47
+ constructor() {
48
+ super(`to equal ${stringifyValue(Number.NEGATIVE_INFINITY)}`, (value) => value === Number.NEGATIVE_INFINITY)
49
+ }
50
+ }
51
+
52
+ export class ToBeNull extends VoidExpectation {
53
+ constructor() {
54
+ super(`to be ${stringifyValue(null)}`, (value) => value === null)
55
+ }
56
+ }
57
+
58
+ export class ToBePositiveInfinity extends VoidExpectation {
59
+ constructor() {
60
+ super(`to equal ${stringifyValue(Number.POSITIVE_INFINITY)}`, (value) => value === Number.POSITIVE_INFINITY)
61
+ }
62
+ }
63
+
64
+ export class ToBeTrue extends VoidExpectation {
65
+ constructor() {
66
+ super(`to be ${stringifyValue(true)}`, (value) => value === true)
67
+ }
68
+ }
69
+
70
+ export class ToBeTruthy extends VoidExpectation {
71
+ constructor() {
72
+ super('to be truthy', (value) => !! value)
73
+ }
74
+ }
75
+
76
+ export class ToBeUndefined extends VoidExpectation {
77
+ constructor() {
78
+ super(`to be ${stringifyValue(undefined)}`, (value) => value === undefined)
79
+ }
80
+ }
package/src/globals.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type * as setup from './execution/setup'
2
+ import type { skip as SkipFunction } from './execution/executable'
3
+ import type { expect as ExpectFunction } from './expectation/expect'
4
+ import type { logging } from '@plugjs/plug'
5
+
6
+ declare global {
7
+ const describe: setup.SuiteFunction
8
+ const fdescribe: setup.SuiteSetup
9
+ const xdescribe: setup.SuiteSetup
10
+
11
+ const it: setup.SpecFunction
12
+ const fit: setup.SpecSetup
13
+ const xit: setup.SpecSetup
14
+
15
+ const afterAll: setup.HookFunction
16
+ const afterEach: setup.HookFunction
17
+ const beforeAll: setup.HookFunction
18
+ const beforeEach: setup.HookFunction
19
+
20
+ const xafterAll: setup.HookSetup
21
+ const xafterEach: setup.HookSetup
22
+ const xbeforeAll: setup.HookSetup
23
+ const xbeforeEach: setup.HookSetup
24
+
25
+ const skip: typeof SkipFunction
26
+
27
+ const expect: typeof ExpectFunction
28
+
29
+ const log: logging.LogFunction
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { installForking } from '@plugjs/plug/fork'
2
+ import { requireResolve } from '@plugjs/plug/paths'
3
+
4
+ import type { ForkOptions } from '@plugjs/plug/fork'
5
+
6
+ /* ========================================================================== *
7
+ * EXPORTED VARIABLES (for when globals is false) *
8
+ * ========================================================================== */
9
+
10
+ export {
11
+ it, fit, xit,
12
+ describe, fdescribe, xdescribe,
13
+ afterAll, afterEach, xafterAll, xafterEach,
14
+ beforeAll, beforeEach, xbeforeAll, xbeforeEach,
15
+ } from './execution/setup'
16
+ export { skip } from './execution/executable'
17
+ export { expect } from './expectation/expect'
18
+
19
+ /* ========================================================================== *
20
+ * EXPORTED OPTIONS TYPE AND PLUG DEFINITION *
21
+ * ========================================================================== */
22
+
23
+ /** Options to construct our {@link Jasmine} plug. */
24
+ export interface TestOptions extends ForkOptions {
25
+ /** Report up to the specified amount of failures (default: `+Infinity`) */
26
+ maxFailures?: number,
27
+ /**
28
+ * Specify whether the variables (`describe`, `it`, `expect`, ...) will be
29
+ * exposed as _global_ variables to tests or not (default: `true`)
30
+ */
31
+ globals?: boolean,
32
+ /**
33
+ * Print differences between expected and actual values from generic errors
34
+ * (e.g. `AssertionError` or _Chai_ expectations) (default: `true`)
35
+ */
36
+ genericErrorDiffs?: boolean,
37
+ }
38
+
39
+ declare module '@plugjs/plug' {
40
+ export interface Pipe {
41
+ /**
42
+ * Run tests.
43
+ *
44
+ * @param options Optional {@link TestOptions | options}.
45
+ */
46
+ test(options?: TestOptions): Promise<undefined>
47
+ }
48
+ }
49
+
50
+ /* ========================================================================== *
51
+ * INSTALL FORKING PLUG *
52
+ * ========================================================================== */
53
+
54
+ installForking('test', requireResolve(__fileurl, './test'), 'Test')
package/src/test.ts ADDED
@@ -0,0 +1,239 @@
1
+ // Reference ourselves, so that the constructor's parameters are correct
2
+ /// <reference path="./index.ts"/>
3
+
4
+ import { AssertionError } from 'node:assert'
5
+
6
+ import { BuildFailure } from '@plugjs/plug'
7
+ import { $blu, $grn, $gry, $ms, $red, $wht, $ylw, log, ERROR, NOTICE, WARN } from '@plugjs/plug/logging'
8
+ import { assert } from '@plugjs/plug/asserts'
9
+
10
+ import { skip, Suite } from './execution/executable'
11
+ import { runSuite } from './execution/executor'
12
+ import { expect } from './expectation/expect'
13
+ import { ExpectationError, stringifyObjectType } from './expectation/types'
14
+ import { printDiff } from './expectation/print'
15
+ import { diff } from './expectation/diff'
16
+ import * as setup from './execution/setup'
17
+
18
+ import type { Files } from '@plugjs/plug/files'
19
+ import type { Logger } from '@plugjs/plug/logging'
20
+ import type { Context, PipeParameters, Plug } from '@plugjs/plug/pipe'
21
+ import type { TestOptions } from './index'
22
+
23
+ const _pending = '\u22EF' // middle ellipsis
24
+ const _success = '\u2714' // heavy check mark
25
+ const _failure = '\u2718' // heavy ballot x
26
+ const _details = '\u2192' // rightwards arrow
27
+
28
+ /** Writes some info about the current {@link Files} being passed around. */
29
+ export class Test implements Plug<void> {
30
+ constructor(...args: PipeParameters<'test'>)
31
+ constructor(private readonly _options: TestOptions = {}) {}
32
+
33
+ async pipe(files: Files, context: Context): Promise<void> {
34
+ assert(files.length, 'No files available for running tests')
35
+
36
+ const {
37
+ globals = true,
38
+ genericErrorDiffs = true,
39
+ maxFailures = Number.POSITIVE_INFINITY,
40
+ } = this._options
41
+
42
+ // Inject globals if we were told to do so...
43
+ if (globals) {
44
+ const anyGlobal = globalThis as any
45
+
46
+ anyGlobal['describe'] = setup.describe
47
+ anyGlobal['fdescribe'] = setup.fdescribe
48
+ anyGlobal['xdescribe'] = setup.xdescribe
49
+ anyGlobal['it'] = setup.it
50
+ anyGlobal['fit'] = setup.fit
51
+ anyGlobal['xit'] = setup.xit
52
+ anyGlobal['afterAll'] = setup.afterAll
53
+ anyGlobal['afterEach'] = setup.afterEach
54
+ anyGlobal['beforeAll'] = setup.beforeAll
55
+ anyGlobal['beforeEach'] = setup.beforeEach
56
+ anyGlobal['xafterAll'] = setup.xafterAll
57
+ anyGlobal['xafterEach'] = setup.xafterEach
58
+ anyGlobal['xbeforeAll'] = setup.xbeforeAll
59
+ anyGlobal['xbeforeEach'] = setup.xbeforeEach
60
+ anyGlobal['skip'] = skip
61
+ anyGlobal['expect'] = expect
62
+ anyGlobal['log'] = log
63
+ }
64
+
65
+ // Create our _root_ Suite
66
+ const suite = new Suite(undefined, '', async () => {
67
+ for (const file of files.absolutePaths()) await import(file)
68
+ })
69
+
70
+ // Setup our suite counts
71
+ await suite.setup()
72
+
73
+ const snum = suite.specs
74
+ const fnum = files.length
75
+ const smsg = snum === 1 ? 'spec' : 'specs'
76
+ const fmsg = fnum === 1 ? 'file' : 'files'
77
+
78
+ assert(snum, 'No specs configured by test files')
79
+
80
+ // Run our suite and setup listeners
81
+ const execution = runSuite(suite)
82
+
83
+ execution.on('suite:start', (current) => {
84
+ if (current.parent === suite) {
85
+ if (suite.flag !== 'only') context.log.notice('')
86
+ context.log.enter(NOTICE, `${$wht(current.name)}`)
87
+ context.log.notice('')
88
+ } else if (current.parent) {
89
+ context.log.enter(NOTICE, `${$blu(_details)} ${$wht(current.name)}`)
90
+ } else {
91
+ context.log.notice(`Running ${$ylw(snum)} ${smsg} from ${$ylw(fnum)} ${fmsg}`)
92
+ if (suite.flag === 'only') context.log.notice('')
93
+ }
94
+ })
95
+
96
+ execution.on('suite:done', (current) => {
97
+ if (current.parent) context.log.leave()
98
+ })
99
+
100
+ execution.on('spec:start', (spec) => {
101
+ context.log.enter(NOTICE, `${$blu(_pending)} ${spec.name}`)
102
+ })
103
+
104
+ execution.on('spec:skip', (spec, ms) => {
105
+ if (suite.flag === 'only') return context.log.leave()
106
+ context.log.leave(WARN, `${$ylw(_pending)} ${spec.name} ${$ms(ms)} ${$gry('[')}${$ylw('skipped')}${$gry(']')}`)
107
+ })
108
+
109
+ execution.on('spec:pass', (spec, ms) => {
110
+ context.log.leave(NOTICE, `${$grn(_success)} ${spec.name} ${$ms(ms)}`)
111
+ })
112
+
113
+ execution.on('spec:fail', (spec, ms, { number }) => {
114
+ context.log.leave(ERROR,
115
+ `${$red(_failure)} ${spec.name} ${$ms(ms)} ` +
116
+ `${$gry('[')}${$red('failed')}${$gry('|')}${$red(`${number}`)}${$gry(']')}`)
117
+ })
118
+
119
+ execution.on('hook:fail', (hook, ms, { number }) => {
120
+ context.log.error(`${$red(_failure)} Hook "${hook.name}" ${$ms(ms)} ` +
121
+ `${$gry('[')}${$red('failed')}${$gry('|')}${$red(`${number}`)}${$gry(']')}`)
122
+ })
123
+
124
+ // Await execution
125
+ const { failed, passed, skipped, failures, time } = await execution.result
126
+
127
+ // Dump all (or a limited number of) failures
128
+ const limit = Math.min(failures.length, maxFailures)
129
+ for (let i = 0; i < limit; i ++) {
130
+ if (i === 0) context.log.error('')
131
+ const { source, error, number } = failures[i]!
132
+
133
+ const names: string[] = [ '' ]
134
+ for (let p = source.parent; p?.parent; p = p.parent) {
135
+ if (p) names.unshift(p.name)
136
+ }
137
+ const details = names.join(` ${$gry(_details)} `) + $wht(source.name)
138
+
139
+ context.log.enter(ERROR, `${$gry('[')}${$red(number)}${$gry(']:')} ${details}`)
140
+ dumpError(context.log, error, genericErrorDiffs)
141
+ context.log.leave()
142
+ }
143
+
144
+ // Epilogue
145
+ const summary: string[] = [ `${passed} ${$gry('passed')}` ]
146
+ if (skipped) summary.push(`${skipped} ${$gry('skipped')}`)
147
+ if (failed) summary.push(`${failed} ${$gry('failed')}`)
148
+ if (failures.length) summary.push(`${failures.length} ${$gry('total failures')}`)
149
+
150
+ const epilogue = ` ${$gry('(')}${summary.join($gry(', '))}${$gry(')')}`
151
+ const message = `Ran ${$ylw(snum)} ${smsg} from ${$ylw(fnum)} ${fmsg}${epilogue} ${$ms(time)}`
152
+
153
+ if (failures.length) {
154
+ context.log.error(message)
155
+ throw new BuildFailure()
156
+ } else if (suite.flag === 'only') {
157
+ context.log.error('')
158
+ context.log.error(message)
159
+ throw new BuildFailure('Suite running in focus ("only") mode')
160
+ } else if (skipped) {
161
+ context.log.warn('')
162
+ context.log.warn(message)
163
+ } else {
164
+ context.log.notice('')
165
+ context.log.notice(message)
166
+ }
167
+ }
168
+ }
169
+
170
+ /* ========================================================================== *
171
+ * ERROR REPORTING *
172
+ * ========================================================================== */
173
+
174
+ function dumpError(log: Logger, error: any, genericErrorDiffs: boolean): void {
175
+ // First and foremost, our own expectation errors
176
+ if (error instanceof ExpectationError) {
177
+ log.enter(ERROR, `${$gry('Expectation Error:')} ${$red(error.message)}`)
178
+ try {
179
+ dumpStack(log, error)
180
+ if (error.diff) printDiff(log, error.diff)
181
+ } finally {
182
+ log.error('')
183
+ log.leave()
184
+ }
185
+
186
+ // Assertion errors are another kind of exception we support
187
+ } else if (error instanceof AssertionError) {
188
+ const [ message = 'Unknown Error', ...lines ] = error.message.split('\n')
189
+ log.enter(ERROR, `${$gry('Assertion Error:')} ${$red(message)}`)
190
+ try {
191
+ dumpStack(log, error)
192
+
193
+ // If we print diffs from generic errors, we take over
194
+ if (genericErrorDiffs) {
195
+ // if this is a generated message ignore all extra lines
196
+ if (! error.generatedMessage) for (const line of lines) log.error(' ', line)
197
+ printDiff(log, diff(error.actual, error.expected))
198
+ } else {
199
+ // trim initial empty lines
200
+ while (lines.length && (! lines[0])) lines.shift()
201
+ for (const line of lines) log.error(' ', line)
202
+ }
203
+ } finally {
204
+ log.error('')
205
+ log.leave()
206
+ }
207
+
208
+ // Any other error also gets printed somewhat nicely
209
+ } else if (error instanceof Error) {
210
+ const message = error.message || 'Unknown Error'
211
+ const type = stringifyObjectType(error)
212
+ log.enter(ERROR, `${$gry(type)}: ${$red(message)}`)
213
+ try {
214
+ dumpStack(log, error)
215
+
216
+ // if there are "actual" or "expected" properties on the error, diff!
217
+ if (genericErrorDiffs && (('actual' in error) || ('expected' in error))) {
218
+ printDiff(log, diff((error as any).actual, (error as any).expected))
219
+ }
220
+ } finally {
221
+ log.error('')
222
+ log.leave()
223
+ }
224
+
225
+ // Anthing else just gets dumped out...
226
+ } else /* coverage ignore next */ {
227
+ // This should never happen, as executor converts evertything to errors...
228
+ log.error($gry('Uknown error:'), error)
229
+ }
230
+ }
231
+
232
+ function dumpStack(log: Logger, error: Error): void {
233
+ if (! error.stack) return log.error('<no stack trace>')
234
+ error.stack
235
+ .split('\n')
236
+ .filter((line) => line.match(/^\s+at\s+/))
237
+ .map((line) => line.trim())
238
+ .forEach((line) => log.error(line))
239
+ }