Skip to content

Commit f6481b1

Browse files
authored
feat: Loggable decorator for piped functions (#17)
`loggable` attaches additional metadata to helper functions for the `.pipe` Command. This includes ensuring the display name of a function regardless of the function being anonymous and also attaches parameters passed. Example: ```ts const getProp = loggable('getProp', prop => obj => obj[prop] cy.wrap({ foo: 'bar' }) .pipe(getProp('foo')) .should('equal', 'bar') ``` This will show the following in the the Cypress Command Log: ``` WRAP {foo: bar} - PIPE getProp("foo") - ASSERT expected bar to equal bar ``` Without using `loggable`, the `PIPE` line would not include arguments passed to `getProp`: ``` - PIPE getProp ``` Fixes #16
1 parent cb2fd1f commit f6481b1

5 files changed

Lines changed: 141 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,31 @@ WRAP {foo: bar}
2828

2929
If the `pipe` was using a `then`, it would fail immediately and wouldn't show the `getFoo` functions anywhere in the Cypress Command Log.
3030

31+
If you care even more about log output and you have more generic functions that curry other functions, you can use `loggable`. That sounds scary, but you might have a function like `getTodoByName` where the function takes the name of the Todo and returns a function that takes a container. For example:
32+
33+
```ts
34+
import { loggable } from 'cypress-pipe'
35+
36+
const getProp = loggable('getProp', prop => obj => obj[prop])
37+
// alternative
38+
const getProp = loggable(prop => function getProp(obj) { return obj[prop] })
39+
40+
cy.wrap({ foo: 'bar' })
41+
.pipe(getProp('foo'))
42+
.should('equal', 'bar')
43+
```
44+
45+
The `loggable` decorator function can either take a `name` as a string, or allow `.pipe` to get it from a named currried function.
46+
47+
The Cypress Log will look like:
48+
49+
```
50+
WRAP {foo: bar}
51+
- PIPE getProp("foo")
52+
- ASSERT expected bar to equal bar
53+
```
54+
55+
3156
This library is a proof of concept, but should be stable. The proposal can be found here: https://github.com/cypress-io/cypress/issues/1548
3257

3358
## Best Practices

‎cypress/integration/example_spec.js‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,41 @@
11
/// <reference types="cypress" />
22
/// <reference path="../../index.d.ts" />
33

4+
const { loggable } = require('../../loggable')
5+
46
const delay = 100 // delay of some retries. Make this larger if you want to see the UI during these times
57

8+
describe('loggable', () => {
9+
context('when only a function is provided', () => {
10+
it('should set __args to equal arguments passed in', () => {
11+
const getProp = loggable(prop => obj => obj[prop])
12+
const fn = getProp('foo', 1, false)
13+
expect(fn.__args).to.deep.equal(['foo', 1, false])
14+
})
15+
16+
it('should set displayName to the name of the function', () => {
17+
const getFoo = loggable(function getFoo() {})
18+
expect(getFoo.displayName).to.equal('getFoo')
19+
})
20+
})
21+
22+
context('when a name is provided', () => {
23+
it('should set the displayName of the function', () => {
24+
const getFoo = loggable('getFoo', () => 'foo')
25+
console.log('getFoo', getFoo)
26+
expect(getFoo.displayName).to.equal('getFoo')
27+
})
28+
})
29+
30+
context('when a curried function is provided', () => {
31+
it('should set the displayName of the curried function', () => {
32+
const getFoo = loggable('getFoo', prop => obj => obj[prop])
33+
const fn = getFoo('foo')
34+
expect(fn.displayName).to.equal('getFoo')
35+
})
36+
})
37+
})
38+
639
describe('pipe()', () => {
740

841
context('when passed a synchronous function', () => {
@@ -31,6 +64,26 @@ describe('pipe()', () => {
3164
.pipe(getFoo)
3265
})
3366

67+
it('should show arguments of loggable functions', (done) => {
68+
const getProp = loggable('getProp', (prop) => (obj) => obj[prop])
69+
const args = ['foo', 1, (foo) => foo, false, [1, 'foo'], { foo: 'bar' }]
70+
71+
cy.on('log:changed', (attrs, log) => {
72+
if (log.get('name') === 'pipe') {
73+
cy.removeAllListeners('log:changed')
74+
75+
expect(log.get('message')).to.eq('getProp("foo", 1, function, false, [1,"foo"], {"foo":"bar"})')
76+
expect(log.invoke('consoleProps')).to.have.property('Arguments')
77+
expect(log.invoke('consoleProps').Arguments).to.deep.equal(args)
78+
done()
79+
}
80+
})
81+
82+
cy.wrap({ foo: 'bar' })
83+
.pipe(getProp(...args))
84+
.should('equal', 'bar')
85+
})
86+
3487
it('should retry until assertion passes', () => {
3588
const obj = { foo: 'bar' }
3689
setTimeout(() => { obj.foo = 'baz' }, delay)

‎index.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ const getElements = $el => {
1717
}
1818
}
1919

20+
/**
21+
* Format the argument according to its type
22+
* @param {any} arg
23+
*/
24+
function formatArg (arg) {
25+
switch (typeof arg) {
26+
case 'function':
27+
return arg.name || 'function'
28+
default:
29+
return JSON.stringify(arg)
30+
}
31+
}
32+
2033
Cypress.Commands.add('pipe', { prevSubject: true }, (subject, fn, options = { }) => {
2134

2235
const getEl = (value) => isJquery(value) ? value : isJquery(subject) ? subject : undefined
@@ -31,16 +44,17 @@ Cypress.Commands.add('pipe', { prevSubject: true }, (subject, fn, options = { })
3144

3245
if (options.log) {
3346
options._log = Cypress.log({
34-
message: fn.name || undefined,
47+
message: (fn.displayName || fn.name || undefined) + (fn.__args ? `(${fn.__args.map(formatArg).join(', ')})` : ''),
3548
$el: getEl(subject), // start the $el with the subject
3649
})
3750
}
3851

3952
const getConsoleProps = (value) => () => ({
4053
Command: 'pipe',
4154
Subject: subject,
42-
Message: fn.name || undefined,
43-
Function: fn.toString(),
55+
Function: fn.displayName || fn.name || undefined,
56+
Arguments: fn.__args || [],
57+
Contents: fn.toString(),
4458
Yielded: isJquery(value) ? getElements(value) : value,
4559
Elements: isJquery(value) ? value.length : undefined,
4660
Duration: performance.now() - now,

‎loggable.d.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Decorate a function with meta-data to be used with `cy.pipe`. Adds
3+
* a display name and arguments of a function.
4+
* @param name The name of the function to be displayed in the Cypress
5+
* Command Log. The parameter is only required if the function passed
6+
* to `cy.pipe` is anonymous
7+
* @param fn Function to decorate
8+
* @example
9+
* const getProp = loggable('getProp', prop => obj => obj[prop])
10+
*/
11+
export function loggable<T extends Function>(name: string, fn: T): T
12+
13+
/**
14+
* Decorate a function with meta-data to be used with `cy.pipe`. Adds
15+
* arguments of a function.
16+
* @param fn Function to decorate
17+
* @example
18+
* const getProp = loggable(prop => function getProp(obj) { return obj[prop] })
19+
*/
20+
export function loggable<T extends Function>(fn: T): T

‎loggable.js‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
function loggable(arg1, arg2) {
2+
if (typeof arg1 === 'string') {
3+
arg2.displayName = arg1
4+
return loggableFn(arg2)
5+
} else if (typeof arg1 === 'function') {
6+
return loggableFn(arg1)
7+
} else {
8+
throw Error('First argument must be a function or string')
9+
}
10+
}
11+
12+
function loggableFn(fn) {
13+
const internalFn = function(...args) {
14+
const value = fn(...args)
15+
if (typeof value === 'function') {
16+
value.displayName = fn.displayName || value.name
17+
value.__args = args
18+
}
19+
return value
20+
}
21+
internalFn.displayName = fn.displayName || fn.name
22+
23+
return internalFn
24+
}
25+
26+
module.exports = { loggable }

0 commit comments

Comments
 (0)