Skip to content

Commit

Permalink
Symbols must be stringified explicitly
Browse files Browse the repository at this point in the history
Apparently (nodejs/node#927 (comment)),
Symbols cannot be implicitly coerced to string.

Template stringification leverages implicit coercion, so we must
stringify propKey before sending it to the template.

Options for explicitly stringifying:

- `String(propKey)`
- `String.prototype.toString(propKey)`

Differences:

```
> String(undefined)
'undefined'
> String.prototype.toString(undefined)
''
```

In this case, `propKey` represents the key for the stubbed function. If
the key is `undefined`, it would be implicitly coerced to `"undefined"`
as the property key. So `String()` is the explicit converstion we want.

Four instances of this issue, each where tdFunction is invoked with a
dynamic name.
  • Loading branch information
jasonkarns committed Nov 18, 2017
1 parent af487ba commit 8b2b9c3
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 4 deletions.
11 changes: 11 additions & 0 deletions regression/src/constructor-test.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ describe 'td.constructor', ->
And -> @fakeInstance.foo.toString() == '[test double for "#foo"]'


if (global.Symbol)
describe 'edge case: being given a Symbol as function name', ->
Given -> @symbolFoo = Symbol('foo')
Given -> @fakeConstructor = td.constructor([@symbolFoo])
Given -> @fakeInstance = new @fakeConstructor('biz')
Then -> @fakeConstructor.prototype[@symbolFoo] == @fakeInstance[@symbolFoo]
And -> td.verify(new @fakeConstructor('biz'))
And -> td.explain(@fakeInstance[@symbolFoo]).isTestDouble == true
And -> @fakeConstructor.toString() == '[test double for "(unnamed constructor)"]'
And -> @fakeInstance.toString() == '[test double instance of constructor]'
And -> @fakeInstance[@symbolFoo].toString() == '[test double for "#Symbol(foo)"]'

describe 'edge case: being given a function without prototypal methods', ->
Given -> @boringFunc = ->
Expand Down
14 changes: 14 additions & 0 deletions regression/src/object-test.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ describe 'td.object', ->
And -> @testDouble.toString() == '[test double object]'
And -> @testDouble.bam.toString() == '[test double for ".bam"]'

if (global.Symbol)
context 'making a test double based on a Symbol', ->
Given -> @symbolFoo = Symbol('foo')
Given -> @testDouble = td.object([@symbolFoo])
When -> td.when(@testDouble[@symbolFoo]()).thenReturn('zing!')
Then -> @testDouble[@symbolFoo]() == 'zing!'
And -> @testDouble.toString() == '[test double object]'
And -> @testDouble[@symbolFoo].toString() == '[test double for ".Symbol(foo)"]'

describe 'passing a function to td.object erroneously (1.x)', ->
When -> try td.object(->) catch e then @result = e
Then -> expect(@result.message).to.contain(
Expand Down Expand Up @@ -52,6 +61,11 @@ describe 'td.object', ->
Given -> @testDouble = td.object()
Then -> @testDouble.toString() == '[test double object]'
Then -> @testDouble.lol.toString() == '[test double for ".lol"]'

if (global.Symbol)
context 'with Symbol propKey', ->
And -> @testDouble[Symbol('foo')].toString() == '[test double for "thing.Symbol(foo)"]'

else
describe 'getting an error message', ->
When -> try
Expand Down
2 changes: 1 addition & 1 deletion src/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var fakeConstructorFromNames = (funcNames) => {
'[test double instance of constructor]'

_.each(funcNames, (funcName) => {
fakeConstructor.prototype[funcName] = tdFunction(`#${funcName}`)
fakeConstructor.prototype[funcName] = tdFunction(`#${String(funcName)}`)
})
})
}
2 changes: 1 addition & 1 deletion src/imitate/create-imitation.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default (original, names) => {
return original
} else {
// TODO: this will become src/function/create and include parent reference instead of name joining here
return tdFunction(names.join('') || '(anonymous function)')
return tdFunction(names.map(String).join('') || '(anonymous function)')
}
} else {
return _.clone(original)
Expand Down
4 changes: 2 additions & 2 deletions src/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var fakeObject = (nameOrType, config) => {

var createTestDoublesForFunctionNames = (names) =>
_.transform(names, (acc, funcName) => {
acc[funcName] = tdFunction(`.${funcName}`)
acc[funcName] = tdFunction(`.${String(funcName)}`)
})

var createTestDoubleViaProxy = (name, config) => {
Expand All @@ -35,7 +35,7 @@ var createTestDoubleViaProxy = (name, config) => {
return new Proxy(obj, {
get (target, propKey, receiver) {
if (!obj.hasOwnProperty(propKey) && !_.includes(config.excludeMethods, propKey)) {
obj[propKey] = tdFunction(`${nameOf(name)}.${propKey}`)
obj[propKey] = tdFunction(`${nameOf(name)}.${String(propKey)}`)
}
return obj[propKey]
}
Expand Down
14 changes: 14 additions & 0 deletions test/unit/imitate/create-imitation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ module.exports = {

assert.deepEqual(result, 'fake thing')
},
'a function with symbols' () {
if (!global.Symbol) return

const someFunc = () => {}
const symFoo = Symbol('foo')
const symBar = Symbol('bar')
td.when(tdFunction('Symbol(foo)Symbol(bar)'))
.thenReturn('fake thing')
td.when(isGenerator(someFunc)).thenReturn(false)

const result = subject(someFunc, [symFoo, symBar])

assert.deepEqual(result, 'fake thing')
},
'other instances': () => {
const original = {a: 'b'}

Expand Down

0 comments on commit 8b2b9c3

Please sign in to comment.