I think Safari is actually behaving correctly (at least partially) in this case. The Web Audio Spec says that ...
A newly-created AudioContext will always begin in the suspended state, and a state change event will be fired whenever the state changes to a different state.
https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-onstatechange
Unfortunately Safari doesn't do the transition to the running state on its own. You have to explicitly ask it to do so.
audioContext.resume();
audioContext.onstatechange = () => console.log(audioContext.state);
The statechange event should fire almost immediately. If you execute this inside the click handler.
The function above would then look like this:
const test = () => {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext();
console.log(audioContext.state); //suspended
audioContext.resume();
audioContext.onstatechange = () => console.log(audioContext.state); // running
}
Interestingly Safari only fires the statechange event if you keep the console.log statement before calling resume().
However there is another hack that you can try to kick of the AudioContext. Just create a simple GainNode.
const test = () => {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext();
audioContext.createGain();
console.log(audioContext.state); // running
}
You can also give standardized-audio-context a try which makes all browsers behave the same in that regard.