| "use strict"; |
| |
| // Different sizes of payloads to test. |
| var smallPayloadSize = 10; |
| var mediumPayloadSize = 10000; |
| var largePayloadSize = 50000; |
| var maxPayloadSize = 65536; // The maximum payload size allowed for a beacon request. |
| |
| // String payloads of various sizes sent by sendbeacon. The format of the payloads is a string: |
| // <numberOfCharacters>:<numberOfCharacters *'s> |
| // ex. "10:**********" |
| var smallPayload = smallPayloadSize + ":" + Array(smallPayloadSize).fill('*').join(""); |
| var mediumPayload = mediumPayloadSize + ":" + Array(mediumPayloadSize).fill('*').join(""); |
| var largePayload = largePayloadSize + ":" + Array(largePayloadSize).fill('*').join(""); |
| // Subtract 6 from maxPayloadSize because 65536 is 5 digits, plus 1 more for the ':' |
| var maxPayload = (maxPayloadSize - 6) + ":" + Array(maxPayloadSize - 6).fill('*').join("") |
| |
| // Test case definitions. |
| // id: String containing the unique name of the test case. |
| // data: Payload object to send through sendbeacon. |
| var noDataTest = { id: "NoData" }; |
| var nullDataTest = { id: "NullData", data: null }; |
| var undefinedDataTest = { id: "UndefinedData", data: undefined }; |
| var smallStringTest = { id: "SmallString", data: smallPayload }; |
| var mediumStringTest = { id: "MediumString", data: mediumPayload }; |
| var largeStringTest = { id: "LargeString", data: largePayload }; |
| var maxStringTest = { id: "MaxString", data: maxPayload }; |
| var emptyBlobTest = { id: "EmptyBlob", data: new Blob() }; |
| var smallBlobTest = { id: "SmallBlob", data: new Blob([smallPayload]) }; |
| var mediumBlobTest = { id: "MediumBlob", data: new Blob([mediumPayload]) }; |
| var largeBlobTest = { id: "LargeBlob", data: new Blob([largePayload]) }; |
| var maxBlobTest = { id: "MaxBlob", data: new Blob([maxPayload]) }; |
| var emptyBufferSourceTest = { id: "EmptyBufferSource", data: new Uint8Array() }; |
| var smallBufferSourceTest = { id: "SmallBufferSource", data: CreateArrayBufferFromPayload(smallPayload) }; |
| var mediumBufferSourceTest = { id: "MediumBufferSource", data: CreateArrayBufferFromPayload(mediumPayload) }; |
| var largeBufferSourceTest = { id: "LargeBufferSource", data: CreateArrayBufferFromPayload(largePayload) }; |
| var maxBufferSourceTest = { id: "MaxBufferSource", data: CreateArrayBufferFromPayload(maxPayload) }; |
| var emptyFormDataTest = { id: "EmptyFormData", data: CreateEmptyFormDataPayload() }; |
| var smallFormDataTest = { id: "SmallFormData", data: CreateFormDataFromPayload(smallPayload) }; |
| var mediumFormDataTest = { id: "MediumFormData", data: CreateFormDataFromPayload(mediumPayload) }; |
| var largeFormDataTest = { id: "LargeFormData", data: CreateFormDataFromPayload(largePayload) }; |
| var smallSafeContentTypeEncodedTest = { id: "SmallSafeContentTypeEncoded", data: new Blob([smallPayload], { type: 'application/x-www-form-urlencoded' }) }; |
| var smallSafeContentTypeFormTest = { id: "SmallSafeContentTypeForm", data: new FormData() }; |
| var smallSafeContentTypeTextTest = { id: "SmallSafeContentTypeText", data: new Blob([smallPayload], { type: 'text/plain' }) }; |
| var smallCORSContentTypeTextTest = { id: "SmallCORSContentTypeText", data: new Blob([smallPayload], { type: 'text/html' }) }; |
| // We don't test maxFormData because the extra multipart separators make it difficult to |
| // calculate a maxPayload. |
| |
| // Test case suites. |
| // Due to quota limits we split the max payload tests into their own bucket. |
| var stringTests = [noDataTest, nullDataTest, undefinedDataTest, smallStringTest, mediumStringTest, largeStringTest]; |
| var stringMaxTest = [maxStringTest]; |
| var blobTests = [emptyBlobTest, smallBlobTest, mediumBlobTest, largeBlobTest]; |
| var blobMaxTest = [maxBlobTest]; |
| var bufferSourceTests = [emptyBufferSourceTest, smallBufferSourceTest, mediumBufferSourceTest, largeBufferSourceTest]; |
| var bufferSourceMaxTest = [maxBufferSourceTest]; |
| var formDataTests = [emptyFormDataTest, smallFormDataTest, mediumFormDataTest, largeFormDataTest]; |
| var formDataMaxTest = [largeFormDataTest]; |
| var contentTypeTests = [smallSafeContentTypeEncodedTest,smallSafeContentTypeFormTest,smallSafeContentTypeTextTest,smallCORSContentTypeTextTest]; |
| var allTests = [].concat(stringTests, stringMaxTest, blobTests, blobMaxTest, bufferSourceTests, bufferSourceMaxTest, formDataTests, formDataMaxTest, contentTypeTests); |
| |
| // This special cross section of test cases is meant to provide a slimmer but reasonably- |
| // representative set of tests for parameterization across variables (e.g. redirect codes, |
| // cors modes, etc.) |
| var sampleTests = [noDataTest, nullDataTest, undefinedDataTest, smallStringTest, smallBlobTest, smallBufferSourceTest, smallFormDataTest, smallSafeContentTypeEncodedTest, smallSafeContentTypeFormTest, smallSafeContentTypeTextTest]; |
| |
| var preflightTests = [smallCORSContentTypeTextTest]; |
| |
| // Build a test lookup table, which is useful when instructing a web worker or an iframe |
| // to run a test, so that we don't have to marshal the entire test case across a process boundary. |
| var testLookup = {}; |
| allTests.forEach(function(testCase) { |
| testLookup[testCase.id] = testCase; |
| }); |
| |
| // Helper function to create an ArrayBuffer representation of a string. |
| function CreateArrayBufferFromPayload(payload) { |
| var length = payload.length; |
| var buffer = new Uint8Array(length); |
| |
| for (var i = 0; i < length; i++) { |
| buffer[i] = payload.charCodeAt(i); |
| } |
| |
| return buffer; |
| } |
| |
| // Helper function to create an empty FormData object. |
| function CreateEmptyFormDataPayload() { |
| if (self.document === undefined) { |
| return null; |
| } |
| |
| return new FormData(); |
| } |
| |
| // Helper function to create a FormData representation of a string. |
| function CreateFormDataFromPayload(payload) { |
| if (self.document === undefined) { |
| return null; |
| } |
| |
| var formData = new FormData(); |
| formData.append("payload", payload); |
| return formData; |
| } |
| |
| // Initializes a session with a client-generated SID. |
| // A "session" is a run of one or more tests. It is used to batch several beacon |
| // tests in a way that isolates the server-side session state and makes it easy |
| // to poll the results of the tests in one request. |
| // testCases: The array of test cases participating in the session. |
| function initSession(testCases) { |
| return { |
| // Provides a unique session identifier to prevent mixing server-side data |
| // with other sessions. |
| id: self.token(), |
| // Dictionary of test name to live testCase object. |
| testCaseLookup: {}, |
| // Array of testCase objects for iteration. |
| testCases: [], |
| // Tracks the total number of tests in the session. |
| totalCount: testCases.length, |
| // Tracks the number of tests for which we have sent the beacon. |
| // When it reaches totalCount, we will start polling for results. |
| sentCount: 0, |
| // Tracks the number of tests for which we have verified the results. |
| // When it reaches sentCount, we will stop polling for results. |
| doneCount: 0, |
| // Helper to add a testCase to the session. |
| add: function add(testCase) { |
| this.testCases.push(testCase); |
| this.testCaseLookup[testCase.id] = testCase; |
| } |
| }; |
| } |
| |
| // Schedules async_test's for each of the test cases, treating them as a single session, |
| // and wires up the continueAfterSendingBeacon() and waitForResults() calls. |
| // The method looks for several "extension" functions in the global scope: |
| // - self.buildId: if present, can change the display name of a test. |
| // - self.buildBaseUrl: if present, can change the base URL of a beacon target URL (this |
| // is the scheme, hostname, and port). |
| // - self.buildTargetUrl: if present, can modify a beacon target URL (for example wrap it). |
| // Parameters: |
| // testCases: An array of test cases. |
| function runTests(testCases) { |
| var session = initSession(testCases); |
| |
| testCases.forEach(function(testCase, testIndex) { |
| // Make a copy of the test case as we'll be storing some metadata on it, |
| // such as which session it belongs to. |
| var testCaseCopy = Object.assign({ session: session }, testCase); |
| |
| // Extension point: generate the test id. |
| var testId = testCase.id; |
| if (self.buildId) { |
| testId = self.buildId(testId); |
| } |
| testCaseCopy.origId = testCaseCopy.id; |
| testCaseCopy.id = testId; |
| testCaseCopy.index = testIndex; |
| |
| session.add(testCaseCopy); |
| |
| // Schedule the sendbeacon in an async test. |
| async_test(function(test) { |
| // Save the testharness.js 'test' object, so that we only have one object |
| // to pass around. |
| testCaseCopy.test = test; |
| |
| // Extension point: generate the beacon URL. |
| var baseUrl = "http://{{host}}:{{ports[http][0]}}"; |
| if (self.buildBaseUrl) { |
| baseUrl = self.buildBaseUrl(baseUrl); |
| } |
| var targetUrl = `${baseUrl}/beacon/resources/beacon.py?cmd=store&sid=${session.id}&tid=${testId}&tidx=${testIndex}`; |
| if (self.buildTargetUrl) { |
| targetUrl = self.buildTargetUrl(targetUrl); |
| } |
| // Attach the URL to the test object for debugging purposes. |
| testCaseCopy.url = targetUrl; |
| |
| // Extension point: send the beacon immediately, or defer. |
| var sendFunc = test.step_func(function sendImmediately(testCase) { |
| var sendResult = sendData(testCase); |
| continueAfterSendingBeacon(sendResult, testCase); |
| }); |
| if (self.sendFunc) { |
| sendFunc = test.step_func(self.sendFunc); |
| } |
| sendFunc(testCaseCopy); |
| }, `Verify 'navigator.sendbeacon()' successfully sends for variant: ${testCaseCopy.id}`); |
| }); |
| } |
| |
| // Sends the beacon for a single test. This step is factored into its own function so that |
| // it can be called from a web worker. It does not check for results. |
| // Note: do not assert from this method, as when called from a worker, we won't have the |
| // full testharness.js test context. Instead return 'false', and the main scope will fail |
| // the test. |
| // Returns the result of the 'sendbeacon()' function call, true or false. |
| function sendData(testCase) { |
| var sent = false; |
| if (testCase.data) { |
| sent = self.navigator.sendBeacon(testCase.url, testCase.data); |
| } else { |
| sent = self.navigator.sendBeacon(testCase.url) |
| } |
| return sent; |
| } |
| |
| // Continues a single test after the beacon has been sent for that test. |
| // Will trigger waitForResults() for the session if this is the last test |
| // in the session to send its beacon. |
| // Assumption: will be called on the test's step_func so that assert's do |
| // not have to be wrapped. |
| function continueAfterSendingBeacon(sendResult, testCase) { |
| var session = testCase.session; |
| |
| // Recaclulate the sent vs. total counts. |
| if (sendResult) { |
| session.sentCount++; |
| } else { |
| session.totalCount--; |
| } |
| |
| // If this was the last test in the session to send its beacon, start polling for results. |
| // Note that we start polling even if just one test in the session sends successfully, |
| // so that if any of the others fail, we still get results from the tests that did send. |
| if (session.sentCount == session.totalCount) { |
| // Exit the current test's execution context in order to run the poll |
| // loop from the harness context. |
| step_timeout(waitForResults.bind(this, session), 0); |
| } |
| |
| // Now fail this test if the beacon did not send. It will be excluded from the poll |
| // loop because of the calculation adjustment above. |
| assert_true(sendResult, "'sendbeacon' function call must succeed"); |
| } |
| |
| // Kicks off an asynchronous monitor to poll the server for test results. As we |
| // verify that the server has received and validated a beacon, we will complete |
| // its testharness test. |
| function waitForResults(session) { |
| // Poll for status until all of the results come in. |
| fetch(`resources/beacon.py?cmd=stat&sid=${session.id}&tidx_min=0&tidx_max=${session.totalCount-1}`).then( |
| function(response) { |
| // Parse as text(), not json(), so that we can log the raw response if |
| // it's invalid. |
| response.text().then(function(rawResponse) { |
| // Check that we got a response we expect and know how to handle. |
| var results; |
| var failure; |
| try { |
| results = JSON.parse(rawResponse); |
| |
| if (results.length === undefined) { |
| failure = `bad validation response schema: rawResponse='${rawResponse}'`; |
| } |
| } catch (e) { |
| failure = `bad validation response: rawResponse='${rawResponse}', got parse error '${e}'`; |
| } |
| |
| if (failure) { |
| // At this point we can't deterministically get results for all of the |
| // tests in the session, so fail the entire session. |
| failSession(session, failure); |
| return; |
| } |
| |
| // The 'stat' call will return an array of zero or more results |
| // of sendbeacon() calls that the server has received and validated. |
| results.forEach(function(result) { |
| var testCase = session.testCaseLookup[result.id]; |
| |
| // While stash.take on the server is supposed to honor read-once, since we're |
| // polling so frequently it is possible that we will receive the same test result |
| // more than once. |
| if (!testCase.done) { |
| testCase.done = true; |
| session.doneCount++; |
| } |
| |
| // Validate that the sendbeacon() was actually sent to the server. |
| var test = testCase.test; |
| test.step(function() { |
| // null JSON values parse as null, not undefined |
| assert_equals(result.error, null, "'sendbeacon' data must not fail validation"); |
| }); |
| |
| test.done(); |
| }); |
| |
| // Continue polling until all of the results come in. |
| if (session.doneCount < session.sentCount) { |
| // testharness.js frowns upon the use of explicit timeouts, but there is no way |
| // around the need to poll for these tests, and there is no use spamming the server |
| // with requestAnimationFrame() just to avoid the use of step_timeout. |
| step_timeout(waitForResults.bind(this, session), 100); |
| } |
| }).catch(function(error) { |
| failSession(session, `unexpected error reading response, error='${error}'`); |
| }); |
| } |
| ); |
| } |
| |
| // Fails all of the tests in the session, meant to be called when an infrastructural |
| // issue prevents us from deterministically completing the individual tests. |
| function failSession(session, reason) { |
| session.testCases.forEach(function(testCase) { |
| var test = testCase.test; |
| test.unreached_func(reason)(); |
| }); |
| } |
| |
| // Creates an iframe on the document's body and runs the sample tests from the iframe. |
| // The iframe is navigated immediately after it sends the data, and the window verifies |
| // that the data is still successfully sent. |
| // funcName: "beacon" to send the data via navigator.sendBeacon(), |
| // "fetch" to send the data via fetch() with the keepalive flag. |
| function runSendInIframeAndNavigateTests(funcName) { |
| var iframe = document.createElement("iframe"); |
| iframe.id = "iframe"; |
| iframe.onload = function() { |
| var tests = Array(); |
| |
| // Clear our onload handler to prevent re-running the tests as we navigate away. |
| this.onload = null; |
| |
| // Implement the self.buildId extension to identify the parameterized |
| // test in the report. |
| self.buildId = function(baseId) { |
| return `${baseId}-${funcName}-NAVIGATE`; |
| }; |
| |
| window.onmessage = function(e) { |
| // The iframe will execute sendData() for us and return the result. |
| var testCase = tests[e.data]; |
| continueAfterSendingBeacon(true /* sendResult */, testCase); |
| }; |
| |
| // Implement the self.sendFunc extension to send the beacon indirectly, |
| // from an iFrame that we can then navigate. |
| self.sendFunc = function(testCase) { |
| var iframeWindow = document.getElementById("iframe").contentWindow; |
| // We run into problems passing the testCase over the document boundary, |
| // because of structured cloning constraints. Instead we'll send over the |
| // test case id, and the iFrame can load the static test case by including |
| // beacon-common.js. |
| tests[testCase.origId] = testCase; |
| iframeWindow.postMessage([testCase.origId, testCase.url, funcName], "*"); |
| }; |
| |
| runTests(sampleTests); |
| }; |
| |
| iframe.src = "navigate.iFrame.sub.html"; |
| document.body.appendChild(iframe); |
| } |