standardized-audio-context-mock
standardized-audio-context-mock copied to clipboard
overloading global AudioContext produces a typescript error
When you use;
import { AudioContext } from 'standardized-audio-context-mock';
global.AudioContext = AudioContext;
typescript gives you an error of;
Type 'typeof AudioContextMock' is not assignable to type '{ new (contextOptions?: AudioContextOptions | undefined): AudioContext; prototype: AudioContext; }'.
Types of property 'prototype' are incompatible.
Type 'AudioContextMock' is missing the following properties from type 'AudioContext': getOutputTimestamp, createScriptProcessor
Hi, I think this is somewhat expected since standardized-audio-context-mock implements the AudioContext type definition from standardized-audio-context. This type definition differs from TypeScript's default definition. Each AudioNode is for example defined as a generic which allows to correctly type its context property.
The default types will define the context property as BaseAudioContext.
const offlineAudioContext = new OfflineAudioContext({length: 1, sampleRate: 44100});
const bufferSourceNode = offlineAudioContext.createBufferSource();
bufferSourceNode.context; // BaseAudioContext
The types that come with standardized-audio-context (and standardized-audio-context-mock) will identify it as an implementation of the IOfflineAudioContext interface instead.
import { OfflineAudioContext } from 'standardized-audio-context';
const offlineAudioContext = new OfflineAudioContext({length: 1, sampleRate: 44100});
const bufferSourceNode = offlineAudioContext.createBufferSource();
bufferSourceNode.context; // IOfflineAudioContext
There are also some properties included in TypeScript's type definition which are not yet implemented in each browser. Since the types from standardized-audio-context actually match the implementation they don't have these properties.
I think this is why a simple type assertion won't work in this case. I think the only thing which would work is a type assertion to any.
import { AudioContext } from 'standardized-audio-context-mock';
global.AudioContext = AudioContext as any;
It's ugly but I don't know a better solution for this. I would. be happy to hear of a more elegant way, though.
So your saying that its something that standardized-audio-context developers could fix?
That's also me. :-) I see this actually as a feature. Part of the reason why I expose custom type definitions is that the ones from TypeScript may or may not match what you actually get in the browser. I think this is a huge problem.
standardized-audio-context provides a runtime check called isSupported(). If that resolves to true it actually means you get the types promised at dev time.
With TypeScript's default types (of any DOM interface) you can only hope that it's actually implemented as TypeScript guessed.
I hope this makes sense.
It does. However, since Typescript is guessing shouldn't we, as developers, also assume?
Are you checking if every method/property is supported in the browser? not just the interface. How can you do that as the spec evolves and support across various browsers differs?
Then, in testing, where no interface exists. You replace those missing methods/properties, effectively creating a "non-standard" environment anyway.
On the otherhand, Jest allows you to mock by overriding existing methods. Therefor the implementations are identical to the ones in the environment and no guess work is done. This is an issue for AudioContext because its not supported in the environment.
By creating an abstraction layer between your code and the actual implementation you are just doing the work of polyfils manually which seems unnecesary. Especially since polyfils are mostly transparent these days.
Am I misunderstanding a fundemental ideology here?
It also seems to me that the ethos of the library breaks a logcial fallacy, when you create a standard platform that itself doesn't adhere to the standards it sets out to match then you are creating something that is only standard to itsself.
Well standardized-audio-context is a polyfill. Or a ponyfill to be 100% correct. It makes sure that any functionality is either supported in all supported browsers or doesn't get exposed at all. That's at least the theory. There are a lot of edge cases.
To make sure this works the test suite contains about 10000 tests which run in each browser. There is also an extensive set of expectation tests which test bugs in various browsers to make sure workarounds can be removed as they become unnecessary.
Hello again, This might be related so I'm going to continue here, if you don't mind. im doing this;
const [oscnode] = registrar.getAudioNodes(context, "OscillatorNode")
console.log(context, oscnode)
context is returning the correctly, but oscnode isn't, im curious if I am passing it the correct string - the documentation isn't clear on what im supposed to do and from what i can deduce from the source code this should work. parhaps you could update the project to accept only specific strings?
EDIT: I was right, but i was calling it before creating the oscillator. heh.
EDIT2: Still need help though.
const [oscnode] = registrar.getAudioNodes<OscillatorNode>(context, "OscillatorNode")
expect(oscnode.start).toHaveBeenCalledTimes(1)
This gives me Typescript errors Generic type 'OscillatorNodeMock<T>' requires 1 type argument(s), The problem is, I don't know what this is supposed to recieve.
As a side effect Typescript then tells me Property 'start' does not exist on type 'AudioNodeMock<any>'
Then finally
expect(received).toHaveBeenCalledTimes(expected)
Matcher error: received value must be a mock or spy function
Received has type: function
Received has value: [Function anonymous]
When looking at the debugger it tells me start() is a proxy. I noticed that in your example you use Chai, but I don't. Would it be possible to allow users to use a different assertion library?
Hi again,
thanks for your suggestions.
parhaps you could update the project to accept only specific strings?
I just did that. When using TypeScript it is now only possible to use the names of the nodes which get actually stored. https://github.com/chrisguttandin/standardized-audio-context-mock/commit/aa39e7651261f54c3016a1ecd18ab4f8c2222d55
This means registrar.getAudioNodes<OscillatorNode>(context, "OscillatorNode") should not be necessary anymore. registrar.getAudioNodes(context, "OscillatorNode") should already infer the correct return type.
Yes, the library uses Sinon.JS and Chai. I would be happy to make it independent of any mocking library. Any ideas how this could be achieved?
Yes, the library uses Sinon.JS and Chai. I would be happy to make it independent of any mocking library. Any ideas how this could be achieved?
No, I think you would have to impliment each testing environment manually?
Currently functions are wrapped in a Sinon.JS stub like this:
stub(this, 'cancelAndHoldAtTime').callThrough();
I think the same functionality with Jest mock functions could be implemented like this:
this.cancelAndHoldAtTime = jest.fn(this.cancelAndHoldAtTime.bind(this));
Maybe standardized-audio-context-mock could expose a function called something like provideMockingFunction which looks somehow like this:
type TMockingFunction = <T extends object>(instance: T, key: keyof T) => void
let providedMockingFunction: null | TMockingFunction = null;
export const provideMockingFunction = (mockingFunction: TMockingFunction) => {
providedMockingFunction = mockingFunction;
}
It could be used like this for Sinon.JS:
provideMockingFunction((instance, key) => stub(instance, key).callThrough());
For Jest it would look like this:
provideMockingFunction((instance, key) => instance[key] = jest.fn(instance[key].bind(instance)));
standardized-audio-context-mock would then just use whatever it got to set up the mocks. Would that help?
its not clear to me how i would use this in my test. is that a 'global' that i would put somewhere in my test then oscnode.start would return a jest function?
Yes, that's what I meant. Probably something that can be configured in a beforeAll() hook or something similar.
that sounds fantastic then. i think that would help.