diff --git a/Libraries/LibWeb/WebAudio/PannerNode.cpp b/Libraries/LibWeb/WebAudio/PannerNode.cpp index 4caec4852a1..a59154c4f41 100644 --- a/Libraries/LibWeb/WebAudio/PannerNode.cpp +++ b/Libraries/LibWeb/WebAudio/PannerNode.cpp @@ -163,4 +163,24 @@ WebIDL::ExceptionOr PannerNode::set_orientation(float x, float y, float z) return {}; } +// https://webaudio.github.io/web-audio-api/#dom-audionode-channelcountmode +WebIDL::ExceptionOr PannerNode::set_channel_count_mode(Bindings::ChannelCountMode mode) +{ + if (mode == Bindings::ChannelCountMode::Max) { + return WebIDL::NotSupportedError::create(realm(), "PannerNode does not support 'max' as channelCountMode."_string); + } + + return AudioNode::set_channel_count_mode(mode); +} + +// https://webaudio.github.io/web-audio-api/#dom-audionode-channelcount +WebIDL::ExceptionOr PannerNode::set_channel_count(WebIDL::UnsignedLong channel_count) +{ + if (channel_count > 2) { + return WebIDL::NotSupportedError::create(realm(), "PannerNode does not support channel count greater than 2"_string); + } + + return AudioNode::set_channel_count(channel_count); +} + } diff --git a/Libraries/LibWeb/WebAudio/PannerNode.h b/Libraries/LibWeb/WebAudio/PannerNode.h index 4be601d6168..f0324a691b6 100644 --- a/Libraries/LibWeb/WebAudio/PannerNode.h +++ b/Libraries/LibWeb/WebAudio/PannerNode.h @@ -77,6 +77,10 @@ public: WebIDL::ExceptionOr set_position(float x, float y, float z); WebIDL::ExceptionOr set_orientation(float x, float y, float z); + // ^AudioNode + virtual WebIDL::ExceptionOr set_channel_count(WebIDL::UnsignedLong) override; + virtual WebIDL::ExceptionOr set_channel_count_mode(Bindings::ChannelCountMode) override; + protected: PannerNode(JS::Realm&, GC::Ref, PannerOptions const& = {}); diff --git a/Tests/LibWeb/Text/expected/wpt-import/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.txt b/Tests/LibWeb/Text/expected/wpt-import/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.txt new file mode 100644 index 00000000000..e2094f8955d --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.txt @@ -0,0 +1,131 @@ +Harness status: OK + +Found 125 tests + +121 Pass +4 Fail +Pass # AUDIT TASK RUNNER STARTED. +Pass Executing "initialize" +Pass Executing "invalid constructor" +Pass Executing "default constructor" +Pass Executing "test AudioNodeOptions" +Pass Executing "constructor with options" +Pass Audit report +Pass > [initialize] +Pass context = new OfflineAudioContext(...) did not throw an exception. +Pass < [initialize] All assertions passed. (total 1 assertions) +Pass > [invalid constructor] +Pass new PannerNode() threw TypeError: "PannerNode() needs one argument". +Pass new PannerNode(1) threw TypeError: "Not an object of type BaseAudioContext". +Pass new PannerNode(context, 42) threw TypeError: "Not an object of type PannerOptions". +Pass < [invalid constructor] All assertions passed. (total 3 assertions) +Pass > [default constructor] +Pass node0 = new PannerNode(context) did not throw an exception. +Pass node0 instanceof PannerNode is equal to true. +Pass node0.numberOfInputs is equal to 1. +Pass node0.numberOfOutputs is equal to 1. +Pass node0.channelCount is equal to 2. +Pass node0.channelCountMode is equal to clamped-max. +Pass node0.channelInterpretation is equal to speakers. +Pass node0.panningModel is equal to equalpower. +Pass node0.positionX.value is equal to 0. +Pass node0.positionY.value is equal to 0. +Pass node0.positionZ.value is equal to 0. +Pass node0.orientationX.value is equal to 1. +Pass node0.orientationY.value is equal to 0. +Pass node0.orientationZ.value is equal to 0. +Pass node0.distanceModel is equal to inverse. +Pass node0.refDistance is equal to 1. +Pass node0.maxDistance is equal to 10000. +Pass node0.rolloffFactor is equal to 1. +Pass node0.coneInnerAngle is equal to 360. +Pass node0.coneOuterAngle is equal to 360. +Pass node0.coneOuterGain is equal to 0. +Pass context.listener.positionX.value is equal to 0. +Pass context.listener.positionY.value is equal to 0. +Pass context.listener.positionZ.value is equal to 0. +Pass context.listener.forwardX.value is equal to 0. +Pass context.listener.forwardY.value is equal to 0. +Pass context.listener.forwardZ.value is equal to -1. +Pass context.listener.upX.value is equal to 0. +Pass context.listener.upY.value is equal to 1. +Pass context.listener.upZ.value is equal to 0. +Pass < [default constructor] All assertions passed. (total 30 assertions) +Pass > [test AudioNodeOptions] +Pass node1 = new PannerNode(c, {"channelCount":1}) did not throw an exception. +Pass node1.channelCount is equal to 1. +Pass node2 = new PannerNode(c, {"channelCount":2}) did not throw an exception. +Pass node2.channelCount is equal to 2. +Pass new PannerNode(c, {"channelCount":0}) threw NotSupportedError: "Invalid channel count". +Pass node.channelCount = 0 threw NotSupportedError: "Invalid channel count". +Pass node.channelCount after setting to 0 is equal to 2. +Pass new PannerNode(c, {"channelCount":3}) threw NotSupportedError: "PannerNode does not support channel count greater than 2". +Pass node.channelCount = 3 threw NotSupportedError: "PannerNode does not support channel count greater than 2". +Pass node.channelCount after setting to 3 is equal to 2. +Pass new PannerNode(c, {"channelCount":99}) threw NotSupportedError: "PannerNode does not support channel count greater than 2". +Pass node.channelCount = 99 threw NotSupportedError: "PannerNode does not support channel count greater than 2". +Pass node.channelCount after setting to 99 is equal to 2. +Pass node3 = new PannerNode(c, {"channelCountMode":"clamped-max"}) did not throw an exception. +Pass node3.channelCountMode is equal to clamped-max. +Pass node4 = new PannerNode(c, {"channelCountMode":"explicit"}) did not throw an exception. +Pass node4.channelCountMode is equal to explicit. +Pass new PannerNode(c, {"channelCountMode":"max"}) threw NotSupportedError: "PannerNode does not support 'max' as channelCountMode.". +Pass node.channelCountMode = max threw NotSupportedError: "PannerNode does not support 'max' as channelCountMode.". +Pass node.channelCountMode after setting to max is equal to clamped-max. +Pass new PannerNode(c, " + JSON.stringify(options) + ") threw TypeError: "Invalid value 'foobar' for enumeration type 'ChannelCountMode'". +Pass node.channelCountMode = foobar did not throw an exception. +Pass node.channelCountMode after setting to foobar is equal to clamped-max. +Pass node5 = new PannerNode(c, {"channelInterpretation":"speakers"}) did not throw an exception. +Pass node5.channelInterpretation is equal to speakers. +Pass node6 = new PannerNode(c, {"channelInterpretation":"discrete"}) did not throw an exception. +Pass node6.channelInterpretation is equal to discrete. +Pass new PannerNode(c, {"channelInterpretation":"foobar"}) threw TypeError: "Invalid value 'foobar' for enumeration type 'ChannelInterpretation'". +Pass new PannerNode(c, {"maxDistance":-1}) threw RangeError: "maxDistance cannot be negative". +Pass node.maxDistance = -1 threw RangeError: "maxDistance cannot be negative". +Pass node.maxDistance after setting to -1 is equal to 10000. +Pass node7 = new PannerNode(c, {"maxDistance":100}) did not throw an exception. +Pass node7.maxDistance is equal to 100. +Pass new PannerNode(c, {"rolloffFactor":-1}) threw RangeError: "rolloffFactor cannot be negative". +Pass node.rolloffFactor = -1 threw RangeError: "rolloffFactor cannot be negative". +Pass node.rolloffFactor after setting to -1 is equal to 1. +Pass node8 = new PannerNode(c, {"rolloffFactor":0}) did not throw an exception. +Pass node8.rolloffFactor is equal to 0. +Pass node8 = new PannerNode(c, {"rolloffFactor":0.5}) did not throw an exception. +Pass node8.rolloffFactor is equal to 0.5. +Pass node8 = new PannerNode(c, {"rolloffFactor":100}) did not throw an exception. +Pass node8.rolloffFactor is equal to 100. +Pass new PannerNode(c, {"coneOuterGain":-1}) threw InvalidStateError: "coneOuterGain must be in the range of [0, 1]". +Pass node.coneOuterGain = -1 threw InvalidStateError: "coneOuterGain must be in the range of [0, 1]". +Pass node.coneOuterGain after setting to -1 is equal to 0. +Pass new PannerNode(c, {"coneOuterGain":1.1}) threw InvalidStateError: "coneOuterGain must be in the range of [0, 1]". +Pass node.coneOuterGain = 1.1 threw InvalidStateError: "coneOuterGain must be in the range of [0, 1]". +Pass node.coneOuterGain after setting to 1.1 is equal to 0. +Pass node9 = new PannerNode(c, {"coneOuterGain":0}) did not throw an exception. +Pass node9.coneOuterGain is equal to 0. +Pass node9 = new PannerNode(c, {"coneOuterGain":0.5}) did not throw an exception. +Pass node9.coneOuterGain is equal to 0.5. +Pass node9 = new PannerNode(c, {"coneOuterGain":1}) did not throw an exception. +Pass node9.coneOuterGain is equal to 1. +Pass < [test AudioNodeOptions] All assertions passed. (total 54 assertions) +Pass > [constructor with options] +Pass node = new PannerNode(c, {"panningModel":"HRTF","positionX":1.4142135623730951,"positionY":2.8284271247461903,"positionZ":4.242640687119286,"orientationX":-1.4142135623730951,"orientationY":-2.8284271247461903,"orientationZ":-4.242640687119286,"distanceModel":"linear","refDistance":3.141592653589793,"maxDistance":6.283185307179586,"rolloffFactor":9.42477796076938,"coneInnerAngle":12.566370614359172,"coneOuterAngle":15.707963267948966,"coneOuterGain":0.3141592653589793}) did not throw an exception. +Pass node instanceof PannerNode is equal to true. +Fail X node.panningModel is not equal to HRTF. Got equalpower. +Pass node.positionX.value is equal to 1.4142135381698608. +Pass node.positionY.value is equal to 2.8284270763397217. +Pass node.positionZ.value is equal to 4.242640495300293. +Pass node.orientationX.value is equal to -1.4142135381698608. +Pass node.orientationY.value is equal to -2.8284270763397217. +Pass node.orientationZ.value is equal to -4.242640495300293. +Fail X node.distanceModel is not equal to linear. Got inverse. +Pass node.refDistance is equal to 3.141592653589793. +Pass node.maxDistance is equal to 6.283185307179586. +Pass node.rolloffFactor is equal to 9.42477796076938. +Pass node.coneInnerAngle is equal to 12.566370614359172. +Pass node.coneOuterAngle is equal to 15.707963267948966. +Pass node.coneOuterGain is equal to 0.3141592653589793. +Pass node.channelCount is equal to 2. +Pass node.channelCountMode is equal to clamped-max. +Pass node.channelInterpretation is equal to speakers. +Fail < [constructor with options] 2 out of 19 assertions were failed. +Fail # AUDIT TASK RUNNER FINISHED: 1 out of 5 tasks were failed. \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/webaudio/resources/audionodeoptions.js b/Tests/LibWeb/Text/input/wpt-import/webaudio/resources/audionodeoptions.js new file mode 100644 index 00000000000..3b7867cabf1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/webaudio/resources/audionodeoptions.js @@ -0,0 +1,292 @@ +// Test that constructor for the node with name |nodeName| handles the +// various possible values for channelCount, channelCountMode, and +// channelInterpretation. + +// The |should| parameter is the test function from new |Audit|. +function testAudioNodeOptions(should, context, nodeName, expectedNodeOptions) { + if (expectedNodeOptions === undefined) + expectedNodeOptions = {}; + let node; + + // Test that we can set channelCount and that errors are thrown for + // invalid values + let testChannelCount = 17; + if (expectedNodeOptions.channelCount) { + testChannelCount = expectedNodeOptions.channelCount.value; + } + should( + () => { + node = new window[nodeName]( + context, Object.assign({}, expectedNodeOptions.additionalOptions, { + channelCount: testChannelCount + })); + }, + 'new ' + nodeName + '(c, {channelCount: ' + testChannelCount + '})') + .notThrow(); + should(node.channelCount, 'node.channelCount').beEqualTo(testChannelCount); + + if (expectedNodeOptions.channelCount && + expectedNodeOptions.channelCount.isFixed) { + // The channel count is fixed. Verify that we throw an error if + // we try to change it. Arbitrarily set the count to be one more + // than the expected value. + testChannelCount = expectedNodeOptions.channelCount.value + 1; + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionalOptions, + {channelCount: testChannelCount})); + }, + 'new ' + nodeName + '(c, {channelCount: ' + testChannelCount + '})') + .throw(DOMException, + expectedNodeOptions.channelCount.exceptionType); + // And test that setting it to the fixed value does not throw. + testChannelCount = expectedNodeOptions.channelCount.value; + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionalOptions, + {channelCount: testChannelCount})); + node.channelCount = testChannelCount; + }, + '(new ' + nodeName + '(c, {channelCount: ' + testChannelCount + '})).channelCount = ' + testChannelCount) + .notThrow(); + } else { + // The channel count is not fixed. Try to set the count to invalid + // values and make sure an error is thrown. + [0, 99].forEach(testValue => { + should(() => { + node = new window[nodeName]( + context, Object.assign({}, expectedNodeOptions.additionalOptions, { + channelCount: testValue + })); + }, `new ${nodeName}(c, {channelCount: ${testValue}})`) + .throw(DOMException, 'NotSupportedError'); + }); + } + + // Test channelCountMode + let testChannelCountMode = 'max'; + if (expectedNodeOptions.channelCountMode) { + testChannelCountMode = expectedNodeOptions.channelCountMode.value; + } + should( + () => { + node = new window[nodeName]( + context, Object.assign({}, expectedNodeOptions.additionalOptions, { + channelCountMode: testChannelCountMode + })); + }, + 'new ' + nodeName + '(c, {channelCountMode: "' + testChannelCountMode + + '"}') + .notThrow(); + should(node.channelCountMode, 'node.channelCountMode') + .beEqualTo(testChannelCountMode); + + if (expectedNodeOptions.channelCountMode && + expectedNodeOptions.channelCountMode.isFixed) { + // Channel count mode is fixed. Test setting to something else throws. + ['max', 'clamped-max', 'explicit'].forEach(testValue => { + if (testValue !== expectedNodeOptions.channelCountMode.value) { + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionalOptions, + {channelCountMode: testValue})); + }, + `new ${nodeName}(c, {channelCountMode: "${testValue}"})`) + .throw(DOMException, + expectedNodeOptions.channelCountMode.exceptionType); + } else { + // Test that explicitly setting the the fixed value is allowed. + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionalOptions, + {channelCountMode: testValue})); + node.channelCountMode = testValue; + }, + `(new ${nodeName}(c, {channelCountMode: "${testValue}"})).channelCountMode = "${testValue}"`) + .notThrow(); + } + }); + } else { + // Mode is not fixed. Verify that we can set the mode to all valid + // values, and that we throw for invalid values. + + let testValues = ['max', 'clamped-max', 'explicit']; + + testValues.forEach(testValue => { + should(() => { + node = new window[nodeName]( + context, Object.assign({}, expectedNodeOptions.additionalOptions, { + channelCountMode: testValue + })); + }, `new ${nodeName}(c, {channelCountMode: "${testValue}"})`).notThrow(); + should( + node.channelCountMode, 'node.channelCountMode after valid setter') + .beEqualTo(testValue); + + }); + + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionalOptions, + {channelCountMode: 'foobar'})); + }, + 'new ' + nodeName + '(c, {channelCountMode: "foobar"}') + .throw(TypeError); + should(node.channelCountMode, 'node.channelCountMode after invalid setter') + .beEqualTo(testValues[testValues.length - 1]); + } + + // Test channelInterpretation + if (expectedNodeOptions.channelInterpretation && + expectedNodeOptions.channelInterpretation.isFixed) { + // The channel interpretation is fixed. Verify that we throw an + // error if we try to change it. + ['speakers', 'discrete'].forEach(testValue => { + if (testValue !== expectedNodeOptions.channelInterpretation.value) { + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionOptions, + {channelInterpretation: testValue})); + }, + `new ${nodeName}(c, {channelInterpretation: "${testValue}"})`) + .throw(DOMException, + expectedNodeOptions.channelCountMode.exceptionType); + } else { + // Check that assigning the fixed value is OK. + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionOptions, + {channelInterpretation: testValue})); + node.channelInterpretation = testValue; + }, + `(new ${nodeName}(c, {channelInterpretation: "${testValue}"})).channelInterpretation = "${testValue}"`) + .notThrow(); + } + }); + } else { + // Channel interpretation is not fixed. Verify that we can set it + // to all possible values. + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionalOptions, + {channelInterpretation: 'speakers'})); + }, + 'new ' + nodeName + '(c, {channelInterpretation: "speakers"})') + .notThrow(); + should(node.channelInterpretation, 'node.channelInterpretation') + .beEqualTo('speakers'); + + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionalOptions, + {channelInterpretation: 'discrete'})); + }, + 'new ' + nodeName + '(c, {channelInterpretation: "discrete"})') + .notThrow(); + should(node.channelInterpretation, 'node.channelInterpretation') + .beEqualTo('discrete'); + + should( + () => { + node = new window[nodeName]( + context, + Object.assign( + {}, expectedNodeOptions.additionalOptions, + {channelInterpretation: 'foobar'})); + }, + 'new ' + nodeName + '(c, {channelInterpretation: "foobar"})') + .throw(TypeError); + should( + node.channelInterpretation, + 'node.channelInterpretation after invalid setter') + .beEqualTo('discrete'); + } +} + +function initializeContext(should) { + let c; + should(() => { + c = new OfflineAudioContext(1, 1, 48000); + }, 'context = new OfflineAudioContext(...)').notThrow(); + + return c; +} + +function testInvalidConstructor(should, name, context) { + should(() => { + new window[name](); + }, 'new ' + name + '()').throw(TypeError); + should(() => { + new window[name](1); + }, 'new ' + name + '(1)').throw(TypeError); + should(() => { + new window[name](context, 42); + }, 'new ' + name + '(context, 42)').throw(TypeError); +} + +function testDefaultConstructor(should, name, context, options) { + let node; + + let message = options.prefix + ' = new ' + name + '(context'; + if (options.constructorOptions) + message += ', ' + JSON.stringify(options.constructorOptions); + message += ')' + + should(() => { + node = new window[name](context, options.constructorOptions); + }, message).notThrow(); + + should(node instanceof window[name], options.prefix + ' instanceof ' + name) + .beEqualTo(true); + should(node.numberOfInputs, options.prefix + '.numberOfInputs') + .beEqualTo(options.numberOfInputs); + should(node.numberOfOutputs, options.prefix + '.numberOfOutputs') + .beEqualTo(options.numberOfOutputs); + should(node.channelCount, options.prefix + '.channelCount') + .beEqualTo(options.channelCount); + should(node.channelCountMode, options.prefix + '.channelCountMode') + .beEqualTo(options.channelCountMode); + should(node.channelInterpretation, options.prefix + '.channelInterpretation') + .beEqualTo(options.channelInterpretation); + + return node; +} + +function testDefaultAttributes(should, node, prefix, items) { + items.forEach((item) => { + let attr = node[item.name]; + if (attr instanceof AudioParam) { + should(attr.value, prefix + '.' + item.name + '.value') + .beEqualTo(item.value); + } else { + should(attr, prefix + '.' + item.name).beEqualTo(item.value); + } + }); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/webaudio/resources/audit-util.js b/Tests/LibWeb/Text/input/wpt-import/webaudio/resources/audit-util.js new file mode 100644 index 00000000000..a4dea796585 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/webaudio/resources/audit-util.js @@ -0,0 +1,195 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + + +/** + * @fileOverview This file includes legacy utility functions for the layout + * test. + */ + +// How many frames in a WebAudio render quantum. +let RENDER_QUANTUM_FRAMES = 128; + +// Compare two arrays (commonly extracted from buffer.getChannelData()) with +// constraints: +// options.thresholdSNR: Minimum allowed SNR between the actual and expected +// signal. The default value is 10000. +// options.thresholdDiffULP: Maximum allowed difference between the actual +// and expected signal in ULP(Unit in the last place). The default is 0. +// options.thresholdDiffCount: Maximum allowed number of sample differences +// which exceeds the threshold. The default is 0. +// options.bitDepth: The expected result is assumed to come from an audio +// file with this number of bits of precision. The default is 16. +function compareBuffersWithConstraints(should, actual, expected, options) { + if (!options) + options = {}; + + // Only print out the message if the lengths are different; the + // expectation is that they are the same, so don't clutter up the + // output. + if (actual.length !== expected.length) { + should( + actual.length === expected.length, + 'Length of actual and expected buffers should match') + .beTrue(); + } + + let maxError = -1; + let diffCount = 0; + let errorPosition = -1; + let thresholdSNR = (options.thresholdSNR || 10000); + + let thresholdDiffULP = (options.thresholdDiffULP || 0); + let thresholdDiffCount = (options.thresholdDiffCount || 0); + + // By default, the bit depth is 16. + let bitDepth = (options.bitDepth || 16); + let scaleFactor = Math.pow(2, bitDepth - 1); + + let noisePower = 0, signalPower = 0; + + for (let i = 0; i < actual.length; i++) { + let diff = actual[i] - expected[i]; + noisePower += diff * diff; + signalPower += expected[i] * expected[i]; + + if (Math.abs(diff) > maxError) { + maxError = Math.abs(diff); + errorPosition = i; + } + + // The reference file is a 16-bit WAV file, so we will almost never get + // an exact match between it and the actual floating-point result. + if (Math.abs(diff) > scaleFactor) + diffCount++; + } + + let snr = 10 * Math.log10(signalPower / noisePower); + let maxErrorULP = maxError * scaleFactor; + + should(snr, 'SNR').beGreaterThanOrEqualTo(thresholdSNR); + + should( + maxErrorULP, + options.prefix + ': Maximum difference (in ulp units (' + bitDepth + + '-bits))') + .beLessThanOrEqualTo(thresholdDiffULP); + + should(diffCount, options.prefix + ': Number of differences between results') + .beLessThanOrEqualTo(thresholdDiffCount); +} + +// Create an impulse in a buffer of length sampleFrameLength +function createImpulseBuffer(context, sampleFrameLength) { + let audioBuffer = + context.createBuffer(1, sampleFrameLength, context.sampleRate); + let n = audioBuffer.length; + let dataL = audioBuffer.getChannelData(0); + + for (let k = 0; k < n; ++k) { + dataL[k] = 0; + } + dataL[0] = 1; + + return audioBuffer; +} + +// Create a buffer of the given length with a linear ramp having values 0 <= x < +// 1. +function createLinearRampBuffer(context, sampleFrameLength) { + let audioBuffer = + context.createBuffer(1, sampleFrameLength, context.sampleRate); + let n = audioBuffer.length; + let dataL = audioBuffer.getChannelData(0); + + for (let i = 0; i < n; ++i) + dataL[i] = i / n; + + return audioBuffer; +} + +// Create an AudioBuffer of length |sampleFrameLength| having a constant value +// |constantValue|. If |constantValue| is a number, the buffer has one channel +// filled with that value. If |constantValue| is an array, the buffer is created +// wit a number of channels equal to the length of the array, and channel k is +// filled with the k'th element of the |constantValue| array. +function createConstantBuffer(context, sampleFrameLength, constantValue) { + let channels; + let values; + + if (typeof constantValue === 'number') { + channels = 1; + values = [constantValue]; + } else { + channels = constantValue.length; + values = constantValue; + } + + let audioBuffer = + context.createBuffer(channels, sampleFrameLength, context.sampleRate); + let n = audioBuffer.length; + + for (let c = 0; c < channels; ++c) { + let data = audioBuffer.getChannelData(c); + for (let i = 0; i < n; ++i) + data[i] = values[c]; + } + + return audioBuffer; +} + +// Create a stereo impulse in a buffer of length sampleFrameLength +function createStereoImpulseBuffer(context, sampleFrameLength) { + let audioBuffer = + context.createBuffer(2, sampleFrameLength, context.sampleRate); + let n = audioBuffer.length; + let dataL = audioBuffer.getChannelData(0); + let dataR = audioBuffer.getChannelData(1); + + for (let k = 0; k < n; ++k) { + dataL[k] = 0; + dataR[k] = 0; + } + dataL[0] = 1; + dataR[0] = 1; + + return audioBuffer; +} + +// Convert time (in seconds) to sample frames. +function timeToSampleFrame(time, sampleRate) { + return Math.floor(0.5 + time * sampleRate); +} + +// Compute the number of sample frames consumed by noteGrainOn with +// the specified |grainOffset|, |duration|, and |sampleRate|. +function grainLengthInSampleFrames(grainOffset, duration, sampleRate) { + let startFrame = timeToSampleFrame(grainOffset, sampleRate); + let endFrame = timeToSampleFrame(grainOffset + duration, sampleRate); + + return endFrame - startFrame; +} + +// True if the number is not an infinity or NaN +function isValidNumber(x) { + return !isNaN(x) && (x != Infinity) && (x != -Infinity); +} + +// Compute the (linear) signal-to-noise ratio between |actual| and +// |expected|. The result is NOT in dB! If the |actual| and +// |expected| have different lengths, the shorter length is used. +function computeSNR(actual, expected) { + let signalPower = 0; + let noisePower = 0; + + let length = Math.min(actual.length, expected.length); + + for (let k = 0; k < length; ++k) { + let diff = actual[k] - expected[k]; + signalPower += expected[k] * expected[k]; + noisePower += diff * diff; + } + + return signalPower / noisePower; +} diff --git a/Tests/LibWeb/Text/input/wpt-import/webaudio/resources/audit.js b/Tests/LibWeb/Text/input/wpt-import/webaudio/resources/audit.js new file mode 100644 index 00000000000..2bb078b1118 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/webaudio/resources/audit.js @@ -0,0 +1,1445 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// See https://github.com/web-platform-tests/wpt/issues/12781 for information on +// the purpose of audit.js, and why testharness.js does not suffice. + +/** + * @fileOverview WebAudio layout test utility library. Built around W3C's + * testharness.js. Includes asynchronous test task manager, + * assertion utilities. + * @dependency testharness.js + */ + + +(function() { + + 'use strict'; + + // Selected methods from testharness.js. + let testharnessProperties = [ + 'test', 'async_test', 'promise_test', 'promise_rejects_js', 'generate_tests', + 'setup', 'done', 'assert_true', 'assert_false' + ]; + + // Check if testharness.js is properly loaded. Throw otherwise. + for (let name in testharnessProperties) { + if (!self.hasOwnProperty(testharnessProperties[name])) + throw new Error('Cannot proceed. testharness.js is not loaded.'); + } +})(); + + +window.Audit = (function() { + + 'use strict'; + + // NOTE: Moving this method (or any other code above) will change the location + // of 'CONSOLE ERROR...' message in the expected text files. + function _logError(message) { + console.error('[audit.js] ' + message); + } + + function _logPassed(message) { + test(function(arg) { + assert_true(true); + }, message); + } + + function _logFailed(message, detail) { + test(function() { + assert_true(false, detail); + }, message); + } + + function _throwException(message) { + throw new Error(message); + } + + // TODO(hongchan): remove this hack after confirming all the tests are + // finished correctly. (crbug.com/708817) + const _testharnessDone = window.done; + window.done = () => { + _throwException('Do NOT call done() method from the test code.'); + }; + + // Generate a descriptive string from a target value in various types. + function _generateDescription(target, options) { + let targetString; + + switch (typeof target) { + case 'object': + // Handle Arrays. + if (target instanceof Array || target instanceof Float32Array || + target instanceof Float64Array || target instanceof Uint8Array) { + let arrayElements = target.length < options.numberOfArrayElements ? + String(target) : + String(target.slice(0, options.numberOfArrayElements)) + '...'; + targetString = '[' + arrayElements + ']'; + } else if (target === null) { + targetString = String(target); + } else { + targetString = '' + String(target).split(/[\s\]]/)[1]; + } + break; + case 'function': + if (Error.isPrototypeOf(target)) { + targetString = "EcmaScript error " + target.name; + } else { + targetString = String(target); + } + break; + default: + targetString = String(target); + break; + } + + return targetString; + } + + // Return a string suitable for printing one failed element in + // |beCloseToArray|. + function _formatFailureEntry(index, actual, expected, abserr, threshold) { + return '\t[' + index + ']\t' + actual.toExponential(16) + '\t' + + expected.toExponential(16) + '\t' + abserr.toExponential(16) + '\t' + + (abserr / Math.abs(expected)).toExponential(16) + '\t' + + threshold.toExponential(16); + } + + // Compute the error threshold criterion for |beCloseToArray| + function _closeToThreshold(abserr, relerr, expected) { + return Math.max(abserr, relerr * Math.abs(expected)); + } + + /** + * @class Should + * @description Assertion subtask for the Audit task. + * @param {Task} parentTask Associated Task object. + * @param {Any} actual Target value to be tested. + * @param {String} actualDescription String description of the test target. + */ + class Should { + constructor(parentTask, actual, actualDescription) { + this._task = parentTask; + + this._actual = actual; + this._actualDescription = (actualDescription || null); + this._expected = null; + this._expectedDescription = null; + + this._detail = ''; + // If true and the test failed, print the actual value at the + // end of the message. + this._printActualForFailure = true; + + this._result = null; + + /** + * @param {Number} numberOfErrors Number of errors to be printed. + * @param {Number} numberOfArrayElements Number of array elements to be + * printed in the test log. + * @param {Boolean} verbose Verbose output from the assertion. + */ + this._options = { + numberOfErrors: 4, + numberOfArrayElements: 16, + verbose: false + }; + } + + _processArguments(args) { + if (args.length === 0) + return; + + if (args.length > 0) + this._expected = args[0]; + + if (typeof args[1] === 'string') { + // case 1: (expected, description, options) + this._expectedDescription = args[1]; + Object.assign(this._options, args[2]); + } else if (typeof args[1] === 'object') { + // case 2: (expected, options) + Object.assign(this._options, args[1]); + } + } + + _buildResultText() { + if (this._result === null) + _throwException('Illegal invocation: the assertion is not finished.'); + + let actualString = _generateDescription(this._actual, this._options); + + // Use generated text when the description is not provided. + if (!this._actualDescription) + this._actualDescription = actualString; + + if (!this._expectedDescription) { + this._expectedDescription = + _generateDescription(this._expected, this._options); + } + + // For the assertion with a single operand. + this._detail = + this._detail.replace(/\$\{actual\}/g, this._actualDescription); + + // If there is a second operand (i.e. expected value), we have to build + // the string for it as well. + this._detail = + this._detail.replace(/\$\{expected\}/g, this._expectedDescription); + + // If there is any property in |_options|, replace the property name + // with the value. + for (let name in this._options) { + if (name === 'numberOfErrors' || name === 'numberOfArrayElements' || + name === 'verbose') { + continue; + } + + // The RegExp key string contains special character. Take care of it. + let re = '\$\{' + name + '\}'; + re = re.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); + this._detail = this._detail.replace( + new RegExp(re, 'g'), _generateDescription(this._options[name])); + } + + // If the test failed, add the actual value at the end. + if (this._result === false && this._printActualForFailure === true) { + this._detail += ' Got ' + actualString + '.'; + } + } + + _finalize() { + if (this._result) { + _logPassed(' ' + this._detail); + } else { + _logFailed('X ' + this._detail); + } + + // This assertion is finished, so update the parent task accordingly. + this._task.update(this); + + // TODO(hongchan): configurable 'detail' message. + } + + _assert(condition, passDetail, failDetail) { + this._result = Boolean(condition); + this._detail = this._result ? passDetail : failDetail; + this._buildResultText(); + this._finalize(); + + return this._result; + } + + get result() { + return this._result; + } + + get detail() { + return this._detail; + } + + /** + * should() assertions. + * + * @example All the assertions can have 1, 2 or 3 arguments: + * should().doAssert(expected); + * should().doAssert(expected, options); + * should().doAssert(expected, expectedDescription, options); + * + * @param {Any} expected Expected value of the assertion. + * @param {String} expectedDescription Description of expected value. + * @param {Object} options Options for assertion. + * @param {Number} options.numberOfErrors Number of errors to be printed. + * (if applicable) + * @param {Number} options.numberOfArrayElements Number of array elements + * to be printed. (if + * applicable) + * @notes Some assertions can have additional options for their specific + * testing. + */ + + /** + * Check if |actual| exists. + * + * @example + * should({}, 'An empty object').exist(); + * @result + * "PASS An empty object does exist." + */ + exist() { + return this._assert( + this._actual !== null && this._actual !== undefined, + '${actual} does exist.', '${actual} does not exist.'); + } + + /** + * Check if |actual| operation wrapped in a function throws an exception + * with a expected error type correctly. |expected| is optional. If it is an + * instance of DOMException, then the description (second argument) can be + * provided to be more strict about the expected exception type. |expected| + * also can be other generic error types such as TypeError, RangeError or + * etc. + * + * @example + * should(() => { let a = b; }, 'A bad code').throw(); + * should(() => { new SomeConstructor(); }, 'A bad construction') + * .throw(DOMException, 'NotSupportedError'); + * should(() => { let c = d; }, 'Assigning d to c') + * .throw(ReferenceError); + * should(() => { let e = f; }, 'Assigning e to f') + * .throw(ReferenceError, { omitErrorMessage: true }); + * + * @result + * "PASS A bad code threw an exception of ReferenceError: b is not + * defined." + * "PASS A bad construction threw DOMException:NotSupportedError." + * "PASS Assigning d to c threw ReferenceError: d is not defined." + * "PASS Assigning e to f threw ReferenceError: [error message + * omitted]." + */ + throw() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let didThrowCorrectly = false; + let passDetail, failDetail; + + try { + // This should throw. + this._actual(); + // Catch did not happen, so the test is failed. + failDetail = '${actual} did not throw an exception.'; + } catch (error) { + let errorMessage = this._options.omitErrorMessage ? + ': [error message omitted]' : + ': "' + error.message + '"'; + if (this._expected === null || this._expected === undefined) { + // The expected error type was not given. + didThrowCorrectly = true; + passDetail = '${actual} threw ' + error.name + errorMessage + '.'; + } else if (this._expected === DOMException && + this._expectedDescription !== undefined) { + // Handles DOMException with an expected exception name. + if (this._expectedDescription === error.name) { + didThrowCorrectly = true; + passDetail = '${actual} threw ${expected}' + errorMessage + '.'; + } else { + didThrowCorrectly = false; + failDetail = + '${actual} threw "' + error.name + '" instead of ${expected}.'; + } + } else if (this._expected == error.constructor) { + // Handler other error types. + didThrowCorrectly = true; + passDetail = '${actual} threw ' + error.name + errorMessage + '.'; + } else { + didThrowCorrectly = false; + failDetail = + '${actual} threw "' + error.name + '" instead of ${expected}.'; + } + } + + return this._assert(didThrowCorrectly, passDetail, failDetail); + } + + /** + * Check if |actual| operation wrapped in a function does not throws an + * exception correctly. + * + * @example + * should(() => { let foo = 'bar'; }, 'let foo = "bar"').notThrow(); + * + * @result + * "PASS let foo = "bar" did not throw an exception." + */ + notThrow() { + this._printActualForFailure = false; + + let didThrowCorrectly = false; + let passDetail, failDetail; + + try { + this._actual(); + passDetail = '${actual} did not throw an exception.'; + } catch (error) { + didThrowCorrectly = true; + failDetail = '${actual} incorrectly threw ' + error.name + ': "' + + error.message + '".'; + } + + return this._assert(!didThrowCorrectly, passDetail, failDetail); + } + + /** + * Check if |actual| promise is resolved correctly. Note that the returned + * result from promise object will be passed to the following then() + * function. + * + * @example + * should('My promise', promise).beResolve().then((result) => { + * log(result); + * }); + * + * @result + * "PASS My promise resolved correctly." + * "FAIL X My promise rejected *INCORRECTLY* with _ERROR_." + */ + beResolved() { + return this._actual.then( + function(result) { + this._assert(true, '${actual} resolved correctly.', null); + return result; + }.bind(this), + function(error) { + this._assert( + false, null, + '${actual} rejected incorrectly with ' + error + '.'); + }.bind(this)); + } + + /** + * Check if |actual| promise is rejected correctly. + * + * @example + * should('My promise', promise).beRejected().then(nextStuff); + * + * @result + * "PASS My promise rejected correctly (with _ERROR_)." + * "FAIL X My promise resolved *INCORRECTLY*." + */ + beRejected() { + return this._actual.then( + function() { + this._assert(false, null, '${actual} resolved incorrectly.'); + }.bind(this), + function(error) { + this._assert( + true, '${actual} rejected correctly with ' + error + '.', null); + }.bind(this)); + } + + /** + * Check if |actual| promise is rejected correctly. + * + * @example + * should(promise, 'My promise').beRejectedWith('_ERROR_').then(); + * + * @result + * "PASS My promise rejected correctly with _ERROR_." + * "FAIL X My promise rejected correctly but got _ACTUAL_ERROR instead of + * _EXPECTED_ERROR_." + * "FAIL X My promise resolved incorrectly." + */ + beRejectedWith() { + this._processArguments(arguments); + + return this._actual.then( + function() { + this._assert(false, null, '${actual} resolved incorrectly.'); + }.bind(this), + function(error) { + if (this._expected !== error.name) { + this._assert( + false, null, + '${actual} rejected correctly but got ' + error.name + + ' instead of ' + this._expected + '.'); + } else { + this._assert( + true, + '${actual} rejected correctly with ' + this._expected + '.', + null); + } + }.bind(this)); + } + + /** + * Check if |actual| is a boolean true. + * + * @example + * should(3 < 5, '3 < 5').beTrue(); + * + * @result + * "PASS 3 < 5 is true." + */ + beTrue() { + return this._assert( + this._actual === true, '${actual} is true.', + '${actual} is not true.'); + } + + /** + * Check if |actual| is a boolean false. + * + * @example + * should(3 > 5, '3 > 5').beFalse(); + * + * @result + * "PASS 3 > 5 is false." + */ + beFalse() { + return this._assert( + this._actual === false, '${actual} is false.', + '${actual} is not false.'); + } + + /** + * Check if |actual| is strictly equal to |expected|. (no type coercion) + * + * @example + * should(1).beEqualTo(1); + * + * @result + * "PASS 1 is equal to 1." + */ + beEqualTo() { + this._processArguments(arguments); + return this._assert( + this._actual === this._expected, '${actual} is equal to ${expected}.', + '${actual} is not equal to ${expected}.'); + } + + /** + * Check if |actual| is not equal to |expected|. + * + * @example + * should(1).notBeEqualTo(2); + * + * @result + * "PASS 1 is not equal to 2." + */ + notBeEqualTo() { + this._processArguments(arguments); + return this._assert( + this._actual !== this._expected, + '${actual} is not equal to ${expected}.', + '${actual} should not be equal to ${expected}.'); + } + + /** + * check if |actual| is NaN + * + * @example + * should(NaN).beNaN(); + * + * @result + * "PASS NaN is NaN" + * + */ + beNaN() { + this._processArguments(arguments); + return this._assert( + isNaN(this._actual), + '${actual} is NaN.', + '${actual} is not NaN but should be.'); + } + + /** + * check if |actual| is NOT NaN + * + * @example + * should(42).notBeNaN(); + * + * @result + * "PASS 42 is not NaN" + * + */ + notBeNaN() { + this._processArguments(arguments); + return this._assert( + !isNaN(this._actual), + '${actual} is not NaN.', + '${actual} is NaN but should not be.'); + } + + /** + * Check if |actual| is greater than |expected|. + * + * @example + * should(2).beGreaterThanOrEqualTo(2); + * + * @result + * "PASS 2 is greater than or equal to 2." + */ + beGreaterThan() { + this._processArguments(arguments); + return this._assert( + this._actual > this._expected, + '${actual} is greater than ${expected}.', + '${actual} is not greater than ${expected}.'); + } + + /** + * Check if |actual| is greater than or equal to |expected|. + * + * @example + * should(2).beGreaterThan(1); + * + * @result + * "PASS 2 is greater than 1." + */ + beGreaterThanOrEqualTo() { + this._processArguments(arguments); + return this._assert( + this._actual >= this._expected, + '${actual} is greater than or equal to ${expected}.', + '${actual} is not greater than or equal to ${expected}.'); + } + + /** + * Check if |actual| is less than |expected|. + * + * @example + * should(1).beLessThan(2); + * + * @result + * "PASS 1 is less than 2." + */ + beLessThan() { + this._processArguments(arguments); + return this._assert( + this._actual < this._expected, '${actual} is less than ${expected}.', + '${actual} is not less than ${expected}.'); + } + + /** + * Check if |actual| is less than or equal to |expected|. + * + * @example + * should(1).beLessThanOrEqualTo(1); + * + * @result + * "PASS 1 is less than or equal to 1." + */ + beLessThanOrEqualTo() { + this._processArguments(arguments); + return this._assert( + this._actual <= this._expected, + '${actual} is less than or equal to ${expected}.', + '${actual} is not less than or equal to ${expected}.'); + } + + /** + * Check if |actual| array is filled with a constant |expected| value. + * + * @example + * should([1, 1, 1]).beConstantValueOf(1); + * + * @result + * "PASS [1,1,1] contains only the constant 1." + */ + beConstantValueOf() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail, failDetail; + let errors = {}; + + let actual = this._actual; + let expected = this._expected; + for (let index = 0; index < actual.length; ++index) { + if (actual[index] !== expected) + errors[index] = actual[index]; + } + + let numberOfErrors = Object.keys(errors).length; + passed = numberOfErrors === 0; + + if (passed) { + passDetail = '${actual} contains only the constant ${expected}.'; + } else { + let counter = 0; + failDetail = + '${actual}: Expected ${expected} for all values but found ' + + numberOfErrors + ' unexpected values: '; + failDetail += '\n\tIndex\tActual'; + for (let errorIndex in errors) { + failDetail += '\n\t[' + errorIndex + ']' + + '\t' + errors[errorIndex]; + if (++counter >= this._options.numberOfErrors) { + failDetail += + '\n\t...and ' + (numberOfErrors - counter) + ' more errors.'; + break; + } + } + } + + return this._assert(passed, passDetail, failDetail); + } + + /** + * Check if |actual| array is not filled with a constant |expected| value. + * + * @example + * should([1, 0, 1]).notBeConstantValueOf(1); + * should([0, 0, 0]).notBeConstantValueOf(0); + * + * @result + * "PASS [1,0,1] is not constantly 1 (contains 1 different value)." + * "FAIL X [0,0,0] should have contain at least one value different + * from 0." + */ + notBeConstantValueOf() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail; + let failDetail; + let differences = {}; + + let actual = this._actual; + let expected = this._expected; + for (let index = 0; index < actual.length; ++index) { + if (actual[index] !== expected) + differences[index] = actual[index]; + } + + let numberOfDifferences = Object.keys(differences).length; + passed = numberOfDifferences > 0; + + if (passed) { + let valueString = numberOfDifferences > 1 ? 'values' : 'value'; + passDetail = '${actual} is not constantly ${expected} (contains ' + + numberOfDifferences + ' different ' + valueString + ').'; + } else { + failDetail = '${actual} should have contain at least one value ' + + 'different from ${expected}.'; + } + + return this._assert(passed, passDetail, failDetail); + } + + /** + * Check if |actual| array is identical to |expected| array element-wise. + * + * @example + * should([1, 2, 3]).beEqualToArray([1, 2, 3]); + * + * @result + * "[1,2,3] is identical to the array [1,2,3]." + */ + beEqualToArray() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail, failDetail; + let errorIndices = []; + + if (this._actual.length !== this._expected.length) { + passed = false; + failDetail = 'The array length does not match.'; + return this._assert(passed, passDetail, failDetail); + } + + let actual = this._actual; + let expected = this._expected; + for (let index = 0; index < actual.length; ++index) { + if (actual[index] !== expected[index]) + errorIndices.push(index); + } + + passed = errorIndices.length === 0; + + if (passed) { + passDetail = '${actual} is identical to the array ${expected}.'; + } else { + let counter = 0; + failDetail = + '${actual} expected to be equal to the array ${expected} ' + + 'but differs in ' + errorIndices.length + ' places:' + + '\n\tIndex\tActual\t\t\tExpected'; + for (let index of errorIndices) { + failDetail += '\n\t[' + index + ']' + + '\t' + this._actual[index].toExponential(16) + '\t' + + this._expected[index].toExponential(16); + if (++counter >= this._options.numberOfErrors) { + failDetail += '\n\t...and ' + (errorIndices.length - counter) + + ' more errors.'; + break; + } + } + } + + return this._assert(passed, passDetail, failDetail); + } + + /** + * Check if |actual| array contains only the values in |expected| in the + * order of values in |expected|. + * + * @example + * Should([1, 1, 3, 3, 2], 'My random array').containValues([1, 3, 2]); + * + * @result + * "PASS [1,1,3,3,2] contains all the expected values in the correct + * order: [1,3,2]. + */ + containValues() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let indexedActual = []; + let firstErrorIndex = null; + + // Collect the unique value sequence from the actual. + for (let i = 0, prev = null; i < this._actual.length; i++) { + if (this._actual[i] !== prev) { + indexedActual.push({index: i, value: this._actual[i]}); + prev = this._actual[i]; + } + } + + // Compare against the expected sequence. + let failMessage = + '${actual} expected to have the value sequence of ${expected} but ' + + 'got '; + if (this._expected.length === indexedActual.length) { + for (let j = 0; j < this._expected.length; j++) { + if (this._expected[j] !== indexedActual[j].value) { + firstErrorIndex = indexedActual[j].index; + passed = false; + failMessage += this._actual[firstErrorIndex] + ' at index ' + + firstErrorIndex + '.'; + break; + } + } + } else { + passed = false; + let indexedValues = indexedActual.map(x => x.value); + failMessage += `${indexedActual.length} values, [${ + indexedValues}], instead of ${this._expected.length}.`; + } + + return this._assert( + passed, + '${actual} contains all the expected values in the correct order: ' + + '${expected}.', + failMessage); + } + + /** + * Check if |actual| array does not have any glitches. Note that |threshold| + * is not optional and is to define the desired threshold value. + * + * @example + * should([0.5, 0.5, 0.55, 0.5, 0.45, 0.5]).notGlitch(0.06); + * + * @result + * "PASS [0.5,0.5,0.55,0.5,0.45,0.5] has no glitch above the threshold + * of 0.06." + * + */ + notGlitch() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail, failDetail; + + let actual = this._actual; + let expected = this._expected; + for (let index = 0; index < actual.length; ++index) { + let diff = Math.abs(actual[index - 1] - actual[index]); + if (diff >= expected) { + passed = false; + failDetail = '${actual} has a glitch at index ' + index + + ' of size ' + diff + '.'; + } + } + + passDetail = + '${actual} has no glitch above the threshold of ${expected}.'; + + return this._assert(passed, passDetail, failDetail); + } + + /** + * Check if |actual| is close to |expected| using the given relative error + * |threshold|. + * + * @example + * should(2.3).beCloseTo(2, { threshold: 0.3 }); + * + * @result + * "PASS 2.3 is 2 within an error of 0.3." + * @param {Object} options Options for assertion. + * @param {Number} options.threshold Threshold value for the comparison. + */ + beCloseTo() { + this._processArguments(arguments); + + // The threshold is relative except when |expected| is zero, in which case + // it is absolute. + let absExpected = this._expected ? Math.abs(this._expected) : 1; + let error = Math.abs(this._actual - this._expected) / absExpected; + + return this._assert( + error <= this._options.threshold, + '${actual} is ${expected} within an error of ${threshold}.', + '${actual} is not close to ${expected} within a relative error of ' + + '${threshold} (RelErr=' + error + ').'); + } + + /** + * Check if |target| array is close to |expected| array element-wise within + * a certain error bound given by the |options|. + * + * The error criterion is: + * abs(actual[k] - expected[k]) < max(absErr, relErr * abs(expected)) + * + * If nothing is given for |options|, then absErr = relErr = 0. If + * absErr = 0, then the error criterion is a relative error. A non-zero + * absErr value produces a mix intended to handle the case where the + * expected value is 0, allowing the target value to differ by absErr from + * the expected. + * + * @param {Number} options.absoluteThreshold Absolute threshold. + * @param {Number} options.relativeThreshold Relative threshold. + */ + beCloseToArray() { + this._processArguments(arguments); + this._printActualForFailure = false; + + let passed = true; + let passDetail, failDetail; + + // Parsing options. + let absErrorThreshold = (this._options.absoluteThreshold || 0); + let relErrorThreshold = (this._options.relativeThreshold || 0); + + // A collection of all of the values that satisfy the error criterion. + // This holds the absolute difference between the target element and the + // expected element. + let errors = {}; + + // Keep track of the max absolute error found. + let maxAbsError = -Infinity, maxAbsErrorIndex = -1; + + // Keep track of the max relative error found, ignoring cases where the + // relative error is Infinity because the expected value is 0. + let maxRelError = -Infinity, maxRelErrorIndex = -1; + + let actual = this._actual; + let expected = this._expected; + + for (let index = 0; index < expected.length; ++index) { + let diff = Math.abs(actual[index] - expected[index]); + let absExpected = Math.abs(expected[index]); + let relError = diff / absExpected; + + if (diff > + Math.max(absErrorThreshold, relErrorThreshold * absExpected)) { + if (diff > maxAbsError) { + maxAbsErrorIndex = index; + maxAbsError = diff; + } + + if (!isNaN(relError) && relError > maxRelError) { + maxRelErrorIndex = index; + maxRelError = relError; + } + + errors[index] = diff; + } + } + + let numberOfErrors = Object.keys(errors).length; + let maxAllowedErrorDetail = JSON.stringify({ + absoluteThreshold: absErrorThreshold, + relativeThreshold: relErrorThreshold + }); + + if (numberOfErrors === 0) { + // The assertion was successful. + passDetail = '${actual} equals ${expected} with an element-wise ' + + 'tolerance of ' + maxAllowedErrorDetail + '.'; + } else { + // Failed. Prepare the detailed failure log. + passed = false; + failDetail = '${actual} does not equal ${expected} with an ' + + 'element-wise tolerance of ' + maxAllowedErrorDetail + '.\n'; + + // Print out actual, expected, absolute error, and relative error. + let counter = 0; + failDetail += '\tIndex\tActual\t\t\tExpected\t\tAbsError' + + '\t\tRelError\t\tTest threshold'; + let printedIndices = []; + for (let index in errors) { + failDetail += + '\n' + + _formatFailureEntry( + index, actual[index], expected[index], errors[index], + _closeToThreshold( + absErrorThreshold, relErrorThreshold, expected[index])); + + printedIndices.push(index); + if (++counter > this._options.numberOfErrors) { + failDetail += + '\n\t...and ' + (numberOfErrors - counter) + ' more errors.'; + break; + } + } + + // Finalize the error log: print out the location of both the maxAbs + // error and the maxRel error so we can adjust thresholds appropriately + // in the test. + failDetail += '\n' + + '\tMax AbsError of ' + maxAbsError.toExponential(16) + + ' at index of ' + maxAbsErrorIndex + '.\n'; + if (printedIndices.find(element => { + return element == maxAbsErrorIndex; + }) === undefined) { + // Print an entry for this index if we haven't already. + failDetail += + _formatFailureEntry( + maxAbsErrorIndex, actual[maxAbsErrorIndex], + expected[maxAbsErrorIndex], errors[maxAbsErrorIndex], + _closeToThreshold( + absErrorThreshold, relErrorThreshold, + expected[maxAbsErrorIndex])) + + '\n'; + } + failDetail += '\tMax RelError of ' + maxRelError.toExponential(16) + + ' at index of ' + maxRelErrorIndex + '.\n'; + if (printedIndices.find(element => { + return element == maxRelErrorIndex; + }) === undefined) { + // Print an entry for this index if we haven't already. + failDetail += + _formatFailureEntry( + maxRelErrorIndex, actual[maxRelErrorIndex], + expected[maxRelErrorIndex], errors[maxRelErrorIndex], + _closeToThreshold( + absErrorThreshold, relErrorThreshold, + expected[maxRelErrorIndex])) + + '\n'; + } + } + + return this._assert(passed, passDetail, failDetail); + } + + /** + * A temporary escape hat for printing an in-task message. The description + * for the |actual| is required to get the message printed properly. + * + * TODO(hongchan): remove this method when the transition from the old Audit + * to the new Audit is completed. + * @example + * should(true, 'The message is').message('truthful!', 'false!'); + * + * @result + * "PASS The message is truthful!" + */ + message(passDetail, failDetail) { + return this._assert( + this._actual, '${actual} ' + passDetail, '${actual} ' + failDetail); + } + + /** + * Check if |expected| property is truly owned by |actual| object. + * + * @example + * should(BaseAudioContext.prototype, + * 'BaseAudioContext.prototype').haveOwnProperty('createGain'); + * + * @result + * "PASS BaseAudioContext.prototype has an own property of + * 'createGain'." + */ + haveOwnProperty() { + this._processArguments(arguments); + + return this._assert( + this._actual.hasOwnProperty(this._expected), + '${actual} has an own property of "${expected}".', + '${actual} does not own the property of "${expected}".'); + } + + + /** + * Check if |expected| property is not owned by |actual| object. + * + * @example + * should(BaseAudioContext.prototype, + * 'BaseAudioContext.prototype') + * .notHaveOwnProperty('startRendering'); + * + * @result + * "PASS BaseAudioContext.prototype does not have an own property of + * 'startRendering'." + */ + notHaveOwnProperty() { + this._processArguments(arguments); + + return this._assert( + !this._actual.hasOwnProperty(this._expected), + '${actual} does not have an own property of "${expected}".', + '${actual} has an own the property of "${expected}".') + } + + + /** + * Check if an object is inherited from a class. This looks up the entire + * prototype chain of a given object and tries to find a match. + * + * @example + * should(sourceNode, 'A buffer source node') + * .inheritFrom('AudioScheduledSourceNode'); + * + * @result + * "PASS A buffer source node inherits from 'AudioScheduledSourceNode'." + */ + inheritFrom() { + this._processArguments(arguments); + + let prototypes = []; + let currentPrototype = Object.getPrototypeOf(this._actual); + while (currentPrototype) { + prototypes.push(currentPrototype.constructor.name); + currentPrototype = Object.getPrototypeOf(currentPrototype); + } + + return this._assert( + prototypes.includes(this._expected), + '${actual} inherits from "${expected}".', + '${actual} does not inherit from "${expected}".'); + } + } + + + // Task Class state enum. + const TaskState = {PENDING: 0, STARTED: 1, FINISHED: 2}; + + + /** + * @class Task + * @description WebAudio testing task. Managed by TaskRunner. + */ + class Task { + /** + * Task constructor. + * @param {Object} taskRunner Reference of associated task runner. + * @param {String||Object} taskLabel Task label if a string is given. This + * parameter can be a dictionary with the + * following fields. + * @param {String} taskLabel.label Task label. + * @param {String} taskLabel.description Description of task. + * @param {Function} taskFunction Task function to be performed. + * @return {Object} Task object. + */ + constructor(taskRunner, taskLabel, taskFunction) { + this._taskRunner = taskRunner; + this._taskFunction = taskFunction; + + if (typeof taskLabel === 'string') { + this._label = taskLabel; + this._description = null; + } else if (typeof taskLabel === 'object') { + if (typeof taskLabel.label !== 'string') { + _throwException('Task.constructor:: task label must be string.'); + } + this._label = taskLabel.label; + this._description = (typeof taskLabel.description === 'string') ? + taskLabel.description : + null; + } else { + _throwException( + 'Task.constructor:: task label must be a string or ' + + 'a dictionary.'); + } + + this._state = TaskState.PENDING; + this._result = true; + + this._totalAssertions = 0; + this._failedAssertions = 0; + } + + get label() { + return this._label; + } + + get state() { + return this._state; + } + + get result() { + return this._result; + } + + // Start the assertion chain. + should(actual, actualDescription) { + // If no argument is given, we cannot proceed. Halt. + if (arguments.length === 0) + _throwException('Task.should:: requires at least 1 argument.'); + + return new Should(this, actual, actualDescription); + } + + // Run this task. |this| task will be passed into the user-supplied test + // task function. + run(harnessTest) { + this._state = TaskState.STARTED; + this._harnessTest = harnessTest; + // Print out the task entry with label and description. + _logPassed( + '> [' + this._label + '] ' + + (this._description ? this._description : '')); + + return new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + let result = this._taskFunction(this, this.should.bind(this)); + if (result && typeof result.then === "function") { + result.then(() => this.done()).catch(reject); + } + }); + } + + // Update the task success based on the individual assertion/test inside. + update(subTask) { + // After one of tests fails within a task, the result is irreversible. + if (subTask.result === false) { + this._result = false; + this._failedAssertions++; + } + + this._totalAssertions++; + } + + // Finish the current task and start the next one if available. + done() { + assert_equals(this._state, TaskState.STARTED) + this._state = TaskState.FINISHED; + + let message = '< [' + this._label + '] '; + + if (this._result) { + message += 'All assertions passed. (total ' + this._totalAssertions + + ' assertions)'; + _logPassed(message); + } else { + message += this._failedAssertions + ' out of ' + this._totalAssertions + + ' assertions were failed.' + _logFailed(message); + } + + this._resolve(); + } + + // Runs |subTask| |time| milliseconds later. |setTimeout| is not allowed in + // WPT linter, so a thin wrapper around the harness's |step_timeout| is + // used here. Returns a Promise which is resolved after |subTask| runs. + timeout(subTask, time) { + return new Promise(resolve => { + this._harnessTest.step_timeout(() => { + let result = subTask(); + if (result && typeof result.then === "function") { + // Chain rejection directly to the harness test Promise, to report + // the rejection against the subtest even when the caller of + // timeout does not handle the rejection. + result.then(resolve, this._reject()); + } else { + resolve(); + } + }, time); + }); + } + + isPassed() { + return this._state === TaskState.FINISHED && this._result; + } + + toString() { + return '"' + this._label + '": ' + this._description; + } + } + + + /** + * @class TaskRunner + * @description WebAudio testing task runner. Manages tasks. + */ + class TaskRunner { + constructor() { + this._tasks = {}; + this._taskSequence = []; + + // Configure testharness.js for the async operation. + setup(new Function(), {explicit_done: true}); + } + + _finish() { + let numberOfFailures = 0; + for (let taskIndex in this._taskSequence) { + let task = this._tasks[this._taskSequence[taskIndex]]; + numberOfFailures += task.result ? 0 : 1; + } + + let prefix = '# AUDIT TASK RUNNER FINISHED: '; + if (numberOfFailures > 0) { + _logFailed( + prefix + numberOfFailures + ' out of ' + this._taskSequence.length + + ' tasks were failed.'); + } else { + _logPassed( + prefix + this._taskSequence.length + ' tasks ran successfully.'); + } + + return Promise.resolve(); + } + + // |taskLabel| can be either a string or a dictionary. See Task constructor + // for the detail. If |taskFunction| returns a thenable, then the task + // is considered complete when the thenable is fulfilled; otherwise the + // task must be completed with an explicit call to |task.done()|. + define(taskLabel, taskFunction) { + let task = new Task(this, taskLabel, taskFunction); + if (this._tasks.hasOwnProperty(task.label)) { + _throwException('Audit.define:: Duplicate task definition.'); + return; + } + this._tasks[task.label] = task; + this._taskSequence.push(task.label); + } + + // Start running all the tasks scheduled. Multiple task names can be passed + // to execute them sequentially. Zero argument will perform all defined + // tasks in the order of definition. + run() { + // Display the beginning of the test suite. + _logPassed('# AUDIT TASK RUNNER STARTED.'); + + // If the argument is specified, override the default task sequence with + // the specified one. + if (arguments.length > 0) { + this._taskSequence = []; + for (let i = 0; i < arguments.length; i++) { + let taskLabel = arguments[i]; + if (!this._tasks.hasOwnProperty(taskLabel)) { + _throwException('Audit.run:: undefined task.'); + } else if (this._taskSequence.includes(taskLabel)) { + _throwException('Audit.run:: duplicate task request.'); + } else { + this._taskSequence.push(taskLabel); + } + } + } + + if (this._taskSequence.length === 0) { + _throwException('Audit.run:: no task to run.'); + return; + } + + for (let taskIndex in this._taskSequence) { + let task = this._tasks[this._taskSequence[taskIndex]]; + // Some tests assume that tasks run in sequence, which is provided by + // promise_test(). + promise_test((t) => task.run(t), `Executing "${task.label}"`); + } + + // Schedule a summary report on completion. + promise_test(() => this._finish(), "Audit report"); + + // From testharness.js. The harness now need not wait for more subtests + // to be added. + _testharnessDone(); + } + } + + /** + * Load file from a given URL and pass ArrayBuffer to the following promise. + * @param {String} fileUrl file URL. + * @return {Promise} + * + * @example + * Audit.loadFileFromUrl('resources/my-sound.ogg').then((response) => { + * audioContext.decodeAudioData(response).then((audioBuffer) => { + * // Do something with AudioBuffer. + * }); + * }); + */ + function loadFileFromUrl(fileUrl) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.open('GET', fileUrl, true); + xhr.responseType = 'arraybuffer'; + + xhr.onload = () => { + // |status = 0| is a workaround for the run_web_test.py server. We are + // speculating the server quits the transaction prematurely without + // completing the request. + if (xhr.status === 200 || xhr.status === 0) { + resolve(xhr.response); + } else { + let errorMessage = 'loadFile: Request failed when loading ' + + fileUrl + '. ' + xhr.statusText + '. (status = ' + xhr.status + + ')'; + if (reject) { + reject(errorMessage); + } else { + new Error(errorMessage); + } + } + }; + + xhr.onerror = (event) => { + let errorMessage = + 'loadFile: Network failure when loading ' + fileUrl + '.'; + if (reject) { + reject(errorMessage); + } else { + new Error(errorMessage); + } + }; + + xhr.send(); + }); + } + + /** + * @class Audit + * @description A WebAudio layout test task manager. + * @example + * let audit = Audit.createTaskRunner(); + * audit.define('first-task', function (task, should) { + * should(someValue).beEqualTo(someValue); + * task.done(); + * }); + * audit.run(); + */ + return { + + /** + * Creates an instance of Audit task runner. + * @param {Object} options Options for task runner. + * @param {Boolean} options.requireResultFile True if the test suite + * requires explicit text + * comparison with the expected + * result file. + */ + createTaskRunner: function(options) { + if (options && options.requireResultFile == true) { + _logError( + 'this test requires the explicit comparison with the ' + + 'expected result when it runs with run_web_tests.py.'); + } + + return new TaskRunner(); + }, + + /** + * Load file from a given URL and pass ArrayBuffer to the following promise. + * See |loadFileFromUrl| method for the detail. + */ + loadFileFromUrl: loadFileFromUrl + + }; + +})(); diff --git a/Tests/LibWeb/Text/input/wpt-import/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html b/Tests/LibWeb/Text/input/wpt-import/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html new file mode 100644 index 00000000000..48efda487e9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html @@ -0,0 +1,468 @@ + + + + + Test Constructor: Panner + + + + + + + + + + +