| // META: script=../../../service-workers/service-worker/resources/test-helpers.sub.js | |
| // META: script=/common/utils.js | |
| // META: script=/common/get-host-info.sub.js | |
| // META: script=resources/utils.js | |
| const { REMOTE_HOST } = get_host_info(); | |
| const BASE_SCOPE = 'resources/basic.html?'; | |
| async function cleanup() { | |
| for (const iframe of document.querySelectorAll('.test-iframe')) { | |
| iframe.parentNode.removeChild(iframe); | |
| } | |
| for (const reg of await navigator.serviceWorker.getRegistrations()) { | |
| await reg.unregister(); | |
| } | |
| } | |
| async function setupRegistration(t, scope) { | |
| await cleanup(); | |
| const reg = await navigator.serviceWorker.register('resources/range-sw.js', { scope }); | |
| await wait_for_state(t, reg.installing, 'activated'); | |
| return reg; | |
| } | |
| function awaitMessage(test, obj, id, maxTimeout) { | |
| return new Promise((resolve, reject) => { | |
| obj.addEventListener('message', function listener(event) { | |
| if (event.data.id !== id) return; | |
| obj.removeEventListener('message', listener); | |
| resolve(event.data); | |
| }); | |
| if (maxTimeout) | |
| test.step_timeout(() => reject("awaiting message timed out"), maxTimeout); | |
| }); | |
| } | |
| promise_test(async t => { | |
| const scope = BASE_SCOPE + Math.random(); | |
| const reg = await setupRegistration(t, scope); | |
| const iframe = await with_iframe(scope); | |
| const w = iframe.contentWindow; | |
| // Trigger a cross-origin range request using media | |
| const url = new URL('long-wav.py?action=range-header-filter-test', w.location); | |
| url.hostname = REMOTE_HOST; | |
| appendAudio(w.document, url); | |
| // See rangeHeaderFilterTest in resources/range-sw.js | |
| await fetch_tests_from_worker(reg.active); | |
| }, `Defer range header filter tests to service worker`); | |
| promise_test(async t => { | |
| const scope = BASE_SCOPE + Math.random(); | |
| const reg = await setupRegistration(t, scope); | |
| const iframe = await with_iframe(scope); | |
| const w = iframe.contentWindow; | |
| // Trigger a cross-origin range request using media | |
| const url = new URL('long-wav.py', w.location); | |
| url.searchParams.set('action', 'range-header-passthrough-test'); | |
| url.searchParams.set('range-received-key', token()); | |
| url.hostname = REMOTE_HOST; | |
| appendAudio(w.document, url); | |
| // See rangeHeaderPassthroughTest in resources/range-sw.js | |
| await fetch_tests_from_worker(reg.active); | |
| }, `Defer range header passthrough tests to service worker`); | |
| promise_test(async t => { | |
| const scope = BASE_SCOPE + Math.random(); | |
| await setupRegistration(t, scope); | |
| const iframe = await with_iframe(scope); | |
| const w = iframe.contentWindow; | |
| const id = Math.random() + ''; | |
| const storedRangeResponse = awaitMessage(t, w.navigator.serviceWorker, id); | |
| // Trigger a cross-origin range request using media | |
| const url = new URL('partial-script.py', w.location); | |
| url.searchParams.set('require-range', '1'); | |
| url.searchParams.set('action', 'store-ranged-response'); | |
| url.searchParams.set('id', id); | |
| url.hostname = REMOTE_HOST; | |
| appendAudio(w.document, url); | |
| await storedRangeResponse; | |
| // Fetching should reject | |
| const fetchPromise = w.fetch('?action=use-stored-ranged-response', { mode: 'no-cors' }); | |
| await promise_rejects_js(t, w.TypeError, fetchPromise); | |
| // Script loading should error too | |
| const loadScriptPromise = loadScript('?action=use-stored-ranged-response', { doc: w.document }); | |
| await promise_rejects_js(t, Error, loadScriptPromise); | |
| assert_false(!!w.scriptExecuted, `Partial response shouldn't be executed`); | |
| }, `Ranged response not allowed following no-cors ranged request`); | |
| promise_test(async t => { | |
| const scope = BASE_SCOPE + Math.random(); | |
| await setupRegistration(t, scope); | |
| const iframe = await with_iframe(scope); | |
| const w = iframe.contentWindow; | |
| const id = Math.random() + ''; | |
| const storedRangeResponse = awaitMessage(t, w.navigator.serviceWorker, id); | |
| // Trigger a range request using media | |
| const url = new URL('partial-script.py', w.location); | |
| url.searchParams.set('require-range', '1'); | |
| url.searchParams.set('action', 'store-ranged-response'); | |
| url.searchParams.set('id', id); | |
| appendAudio(w.document, url); | |
| await storedRangeResponse; | |
| // This should not throw | |
| await w.fetch('?action=use-stored-ranged-response'); | |
| // This shouldn't throw either | |
| await loadScript('?action=use-stored-ranged-response', { doc: w.document }); | |
| assert_true(w.scriptExecuted, `Partial response should be executed`); | |
| }, `Non-opaque ranged response executed`); | |
| promise_test(async t => { | |
| const scope = BASE_SCOPE + Math.random(); | |
| await setupRegistration(t, scope); | |
| const iframe = await with_iframe(scope); | |
| const w = iframe.contentWindow; | |
| const fetchId = Math.random() + ''; | |
| const fetchBroadcast = awaitMessage(t, w.navigator.serviceWorker, fetchId); | |
| const audioId = Math.random() + ''; | |
| const audioBroadcast = awaitMessage(t, w.navigator.serviceWorker, audioId); | |
| const url = new URL('long-wav.py', w.location); | |
| url.searchParams.set('action', 'broadcast-accept-encoding'); | |
| url.searchParams.set('id', fetchId); | |
| await w.fetch(url, { | |
| headers: { Range: 'bytes=0-10' } | |
| }); | |
| assert_equals((await fetchBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for fetch"); | |
| url.searchParams.set('id', audioId); | |
| appendAudio(w.document, url); | |
| assert_equals((await audioBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for media"); | |
| }, `Accept-Encoding should not appear in a service worker`); | |
| promise_test(async t => { | |
| const scope = BASE_SCOPE + Math.random(); | |
| await setupRegistration(t, scope); | |
| const iframe = await with_iframe(scope); | |
| const w = iframe.contentWindow; | |
| const length = 100; | |
| const count = 3; | |
| const counts = {}; | |
| // test a single range request size | |
| async function testSizedRange(size, partialResponseCode) { | |
| const rangeId = Math.random() + ''; | |
| const rangeBroadcast = awaitMessage(t, w.navigator.serviceWorker, rangeId, 1000); | |
| // Create a bogus audio element to trick the browser into sending | |
| // cross-origin range requests that can be manipulated by the service worker. | |
| const sound_url = new URL('partial-text.py', w.location); | |
| sound_url.hostname = REMOTE_HOST; | |
| sound_url.searchParams.set('action', 'record-media-range-request'); | |
| sound_url.searchParams.set('length', length); | |
| sound_url.searchParams.set('size', size); | |
| sound_url.searchParams.set('partial', partialResponseCode); | |
| sound_url.searchParams.set('id', rangeId); | |
| sound_url.searchParams.set('type', 'audio/mp4'); | |
| appendAudio(w.document, sound_url); | |
| // wait for the range requests to happen | |
| await rangeBroadcast; | |
| // Create multiple preload requests and count the number of resource timing | |
| // entries that get created to make sure 206 and 416 range responses are treated | |
| // the same. | |
| const url = new URL('partial-text.py', w.location); | |
| url.searchParams.set('action', 'use-media-range-request'); | |
| url.searchParams.set('size', size); | |
| url.searchParams.set('type', 'audio/mp4'); | |
| counts['size' + size] = 0; | |
| for (let i = 0; i < count; i++) { | |
| await preloadImage(url, { doc: w.document }); | |
| } | |
| } | |
| // Test range requests from 1 smaller than the correct size to 1 larger than | |
| // the correct size to exercise the various permutations using the default 206 | |
| // response code for successful range requests. | |
| for (let size = length - 1; size <= length + 1; size++) { | |
| await testSizedRange(size, '206'); | |
| } | |
| // Test a successful range request using a 200 response. | |
| await testSizedRange(length - 2, '200'); | |
| // Check the resource timing entries and count the reported number of fetches of each type | |
| const resources = w.performance.getEntriesByType("resource"); | |
| for (const entry of resources) { | |
| const url = new URL(entry.name); | |
| if (url.searchParams.has('action') && | |
| url.searchParams.get('action') == 'use-media-range-request' && | |
| url.searchParams.has('size')) { | |
| counts['size' + url.searchParams.get('size')]++; | |
| } | |
| } | |
| // Make sure there are a non-zero number of preload requests and they are all the same | |
| let counts_valid = true; | |
| const first = 'size' + (length - 2); | |
| for (let size = length - 2; size <= length + 1; size++) { | |
| let key = 'size' + size; | |
| if (!(key in counts) || counts[key] <= 0 || counts[key] != counts[first]) { | |
| counts_valid = false; | |
| break; | |
| } | |
| } | |
| assert_true(counts_valid, `Opaque range request preloads were different for error and success`); | |
| }, `Opaque range preload successes and failures should be indistinguishable`); |