Skip to content

Commit 7eb8151

Browse files
committed
chore(tests): fix notifier
Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
1 parent 9fae8b6 commit 7eb8151

File tree

1 file changed

+121
-151
lines changed

1 file changed

+121
-151
lines changed

__tests__/reporters/notifier.mts

Lines changed: 121 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,217 +1,187 @@
11
/**
2-
* @file Reporters - VerboseReporter
3-
* @module tests/reporters/VerboseReporter
2+
* @file Reporters - Notifier
3+
* @module tests/reporters/Notifier
44
* @see https://vitest.dev/advanced/reporters#exported-reporters
55
*/
66

7-
import type { Task, TaskResultPack } from '@vitest/runner'
8-
import { getNames, getTests } from '@vitest/runner/utils'
9-
import colors, { type Colors } from 'tinyrainbow'
10-
import type { RunnerTask, RunnerTestFile } from 'vitest'
11-
import { DefaultReporter, type Reporter } from 'vitest/reporters'
7+
import type { Test } from '@vitest/runner'
8+
import { getTests } from '@vitest/runner/utils'
9+
import ci from 'is-ci'
10+
import notifier from 'node-notifier'
11+
import type { Notification } from 'node-notifier/notifiers/notificationcenter'
12+
import { performance } from 'node:perf_hooks'
13+
import { promisify } from 'node:util'
14+
import { dedent } from 'ts-dedent'
15+
import type { RunnerTestFile } from 'vitest'
16+
import type { Vitest } from 'vitest/node'
17+
import type { Reporter } from 'vitest/reporters'
1218

1319
/**
14-
* Verbose reporter.
20+
* Test report summary notifier.
1521
*
16-
* @see {@linkcode DefaultReporter}
1722
* @see {@linkcode Reporter}
1823
*
19-
* @extends {DefaultReporter}
2024
* @implements {Reporter}
2125
*/
22-
class VerboseReporter extends DefaultReporter implements Reporter {
26+
class Notifier implements Reporter {
2327
/**
24-
* Color functions map.
28+
* Reporter context.
29+
*
30+
* @see {@linkcode Vitest}
2531
*
2632
* @public
2733
* @instance
28-
* @member {Colors} colors
34+
* @member {Vitest} ctx
2935
*/
30-
public colors: Colors
36+
public ctx!: Vitest
3137

3238
/**
33-
* Create a new verbose reporter.
34-
*/
35-
constructor() {
36-
super({ summary: true })
37-
38-
this.colors = colors
39-
this.renderSucceed = true
40-
this.verbose = true
41-
}
42-
43-
/**
44-
* Format a project `name`.
39+
* Test run end time (in milliseconds).
4540
*
46-
* @protected
41+
* @public
4742
* @instance
48-
*
49-
* @param {string | null | undefined} name
50-
* The project name to format
51-
* @param {boolean | null | undefined} dim
52-
* Dim formattted project name?
53-
* @return {string}
54-
* Formatted project name
43+
* @member {number} end
5544
*/
56-
protected formatProjectName(
57-
name: string | null | undefined,
58-
dim?: boolean | null | undefined
59-
): string {
60-
if (name) {
61-
name = this.colors.magenta(`[${name}]`)
62-
if (dim) name = this.colors.dim(name)
63-
return name
64-
}
65-
66-
return ''
67-
}
45+
public end!: number
6846

6947
/**
70-
* Get a symbol representing `task`.
48+
* Test run start time (in milliseconds).
7149
*
72-
* @see {@linkcode RunnerTask}
73-
*
74-
* @protected
50+
* @public
7551
* @instance
76-
*
77-
* @param {RunnerTask} task
78-
* The runner task to handle
79-
* @return {string}
80-
* Task state symbol
52+
* @member {number} start
8153
*/
82-
protected getTaskSymbol(task: RunnerTask): string {
83-
if (task.mode === 'skip') return this.colors.dim(this.colors.gray('↓'))
84-
85-
if (task.mode === 'todo') return this.colors.yellow('→')
86-
87-
if (!task.result) return this.colors.gray('.')
88-
89-
if (task.result.state === 'pass') {
90-
return this.colors.green(task.meta.benchmark ? '·' : '✓')
91-
}
92-
93-
if (task.result.state === 'fail') {
94-
return this.colors.red(task.type === 'suite' ? '❯' : '✖')
95-
}
96-
97-
return ''
98-
}
54+
public start!: number
9955

10056
/**
101-
* Print tasks.
57+
* Send a notification after all tests have ran (in non ci/cd environments).
10258
*
10359
* @see {@linkcode RunnerTestFile}
10460
*
10561
* @public
106-
* @override
10762
* @instance
10863
*
109-
* @param {RunnerTestFile[] | undefined} [files]
64+
* @async
65+
*
66+
* @param {RunnerTestFile[] | undefined} [files=this.ctx.state.getFiles()]
11067
* List of test files
111-
* @param {unknown[] | undefined} [errors]
68+
* @param {unknown[] | undefined} [errors=this.ctx.state.getUnhandledErrors()]
11269
* List of unhandled errors
11370
* @return {undefined}
11471
*/
115-
public override onFinished(
116-
files?: RunnerTestFile[] | undefined,
117-
errors?: unknown[] | undefined
118-
): undefined {
119-
if (files) { for (const task of files) this.printTask(task, true) }
120-
return void super.onFinished(files, errors)
72+
public async onFinished(
73+
files: RunnerTestFile[] = this.ctx.state.getFiles(),
74+
errors: unknown[] = this.ctx.state.getUnhandledErrors()
75+
): Promise<undefined> {
76+
this.end = performance.now()
77+
return void await (ci || this.reportSummary(files, errors))
12178
}
12279

12380
/**
124-
* Handle task updates.
81+
* Initialize the reporter.
12582
*
126-
* @see {@linkcode TaskResultPack}
83+
* @see {@linkcode Vitest}
12784
*
12885
* @public
129-
* @override
13086
* @instance
13187
*
132-
* @param {TaskResultPack[]} packs
133-
* List of task result packs
88+
* @param {Vitest} ctx
89+
* Reporter context
13490
* @return {undefined}
13591
*/
136-
public override onTaskUpdate(packs: TaskResultPack[]): undefined {
137-
return void (this.isTTY && void super.onTaskUpdate(packs))
92+
public onInit(ctx: Vitest): undefined {
93+
return void (this.ctx = ctx, this.start = performance.now())
13894
}
13995

14096
/**
141-
* Print `task`.
97+
* Send a notification.
14298
*
143-
* @see {@linkcode Task}
99+
* @see {@linkcode RunnerTestFile}
144100
*
145-
* @protected
146-
* @override
101+
* @public
147102
* @instance
148103
*
149-
* @param {Task | null | undefined} task
150-
* The task to handle
151-
* @param {boolean | null | undefined} [force]
152-
* Print `task` even when {@linkcode isTTY} is `false`?
153-
* @return {undefined}
104+
* @async
105+
*
106+
* @param {RunnerTestFile[] | undefined} [files=this.ctx.state.getFiles()]
107+
* List of test files
108+
* @param {unknown[] | undefined} [errors=this.ctx.state.getUnhandledErrors()]
109+
* List of unhandled errors
110+
* @return {Promise<undefined>}
154111
*/
155-
protected override printTask(
156-
task: Task | null | undefined,
157-
force?: boolean | null | undefined
158-
): undefined {
159-
if (
160-
(!this.isTTY || force) &&
161-
task?.result?.state &&
162-
task.result.state !== 'queued' &&
163-
task.result.state !== 'run'
164-
) {
112+
public async reportSummary(
113+
files: RunnerTestFile[] = this.ctx.state.getFiles(),
114+
errors: unknown[] = this.ctx.state.getUnhandledErrors()
115+
): Promise<undefined> {
116+
/**
117+
* Tests that have been run.
118+
*
119+
* @const {Test[]} tests
120+
*/
121+
const tests: Test[] = getTests(files)
122+
123+
/**
124+
* Total number of failed tests.
125+
*
126+
* @const {number} fails
127+
*/
128+
const fails: number = tests.filter(t => t.result?.state === 'fail').length
129+
130+
/**
131+
* Total number of passed tests.
132+
*
133+
* @const {number} passes
134+
*/
135+
const passes: number = tests.filter(t => t.result?.state === 'pass').length
136+
137+
/**
138+
* Notification message.
139+
*
140+
* @var {string} message
141+
*/
142+
let message: string = ''
143+
144+
/**
145+
* Notification title.
146+
*
147+
* @var {string} title
148+
*/
149+
let title: string = ''
150+
151+
// get notification title and message based on number of failed tests
152+
if (fails || errors.length > 0) {
153+
message = dedent`
154+
${fails} of ${tests.length} tests failed
155+
${errors.length} unhandled errors
156+
`
157+
158+
title = '\u274C Failed'
159+
} else {
165160
/**
166-
* Task skipped?
161+
* Time to run all tests (in milliseconds).
167162
*
168-
* @const {boolean} skip
163+
* @const {number} time
169164
*/
170-
const skip: boolean = task.mode === 'skip'
165+
const time: number = this.end - this.start
171166

172-
/**
173-
* Printed task.
174-
*
175-
* @var {string} state
176-
*/
177-
let state: string = ''
178-
179-
state = ' '.repeat(getNames(task).length * 2)
180-
state += this.getTaskSymbol(task) + ' '
181-
182-
if (task.type !== 'suite') {
183-
this.log(state += skip ? this.colors.blackBright(task.name) : task.name)
184-
} else {
185-
/**
186-
* Suite title.
187-
*
188-
* @var {string} suite
189-
*/
190-
let suite: string = ''
191-
192-
if ('filepath' in task) {
193-
suite = task.file.name
194-
195-
if (task.file.projectName) {
196-
state += this.formatProjectName(task.file.projectName, skip) + ' '
197-
}
198-
} else {
199-
suite = task.name
200-
}
201-
202-
suite += ` (${getTests(task).length})`
203-
state += skip ? this.colors.blackBright(suite) : suite
204-
205-
this.log(state)
206-
207-
if (!skip) {
208-
for (const subtask of task.tasks) void this.printTask(subtask, force)
209-
}
167+
message = dedent`
168+
${passes} tests passed in ${
169+
time > 1000
170+
? `${(time / 1000).toFixed(2)}ms`
171+
: `${Math.round(time)}ms`
210172
}
173+
`
174+
175+
title = '\u2705 Passed'
211176
}
212177

213-
return void task
178+
return void await promisify<Notification>(notifier.notify.bind(notifier))({
179+
message,
180+
sound: true,
181+
timeout: 15,
182+
title
183+
})
214184
}
215185
}
216186

217-
export default VerboseReporter
187+
export default Notifier

0 commit comments

Comments
 (0)