<!DOCTYPE html>
<title>Service Worker: Navigation redirection</title>
<meta name="timeout" content="long">
<!-- empty variant tests document.location and intercepted URLs -->
<meta name="variant" content="">
<!-- client variant tests the Clients API (resultingClientId and Client.url) -->
<meta name="variant" content="?client">

<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<script src="resources/test-helpers.sub.js"></script>
<body>
<script>
const host_info = get_host_info();

// This test registers three Service Workers at SCOPE1, SCOPE2 and
// OTHER_ORIGIN_SCOPE. And checks the redirected page's URL and the requests
// which are intercepted by Service Worker while loading redirect page.
const BASE_URL = host_info['HTTPS_ORIGIN'] + base_path();
const OTHER_BASE_URL = host_info['HTTPS_REMOTE_ORIGIN'] + base_path();

const SCOPE1 = BASE_URL + 'resources/navigation-redirect-scope1.py?';
const SCOPE2 = BASE_URL + 'resources/navigation-redirect-scope2.py?';
const OUT_SCOPE = BASE_URL + 'resources/navigation-redirect-out-scope.py?';
const SCRIPT = 'resources/redirect-worker.js';

const OTHER_ORIGIN_IFRAME_URL =
      OTHER_BASE_URL + 'resources/navigation-redirect-other-origin.html';
const OTHER_ORIGIN_SCOPE =
      OTHER_BASE_URL + 'resources/navigation-redirect-scope1.py?';
const OTHER_ORIGIN_OUT_SCOPE =
      OTHER_BASE_URL + 'resources/navigation-redirect-out-scope.py?';

let registrations;
let workers;
let other_origin_frame;
let message_resolvers = {};
let next_message_id = 0;

promise_test(async t  => {
  // In this frame we register a service worker at OTHER_ORIGIN_SCOPE.
  // And will use this frame to communicate with the worker.
  other_origin_frame = await with_iframe(OTHER_ORIGIN_IFRAME_URL);

  // Register same-origin service workers.
  registrations = await Promise.all([
      service_worker_unregister_and_register(t, SCRIPT, SCOPE1),
      service_worker_unregister_and_register(t, SCRIPT, SCOPE2)]);

  // Wait for all workers to activate.
  workers = registrations.map(get_effective_worker);
  return Promise.all([
      wait_for_state(t, workers[0], 'activated'),
      wait_for_state(t, workers[1], 'activated'),
      // This promise will resolve when |wait_for_worker_promise|
      // in OTHER_ORIGIN_IFRAME_URL resolves.
      send_to_iframe(other_origin_frame, {command: 'wait_for_worker'})]);
}, 'initialize global state');

function get_effective_worker(registration) {
  if (registration.active)
    return registration.active;
  if (registration.waiting)
    return registration.waiting;
  if (registration.installing)
    return registration.installing;
}

async function check_all_intercepted_urls(expected_urls) {
  const urls = [];
  urls.push(await get_intercepted_urls(workers[0]));
  urls.push(await get_intercepted_urls(workers[1]));
  // Gets the request URLs which are intercepted by OTHER_ORIGIN_SCOPE's
  // SW. This promise will resolve when get_request_infos() in
  // OTHER_ORIGIN_IFRAME_URL resolves.
  const request_infos = await send_to_iframe(other_origin_frame,
                                             {command: 'get_request_infos'});
  urls.push(request_infos.map(info => { return info.url; }));

  assert_object_equals(urls, expected_urls, 'Intercepted URLs should match.');
}

// Checks |clients| returned from a worker. Only the client matching
// |expected_final_client_tag| should be found. Returns true if a client was
// found. Note that the final client is not necessarily found by this worker,
// if the client is cross-origin.
//
// |clients| is an object like:
// {x: {found: true, id: id1, url: url1}, b: {found: false}}
function check_clients(clients,
                       expected_id,
                       expected_url,
                       expected_final_client_tag,
                       worker_name) {
  let found = false;
  Object.keys(clients).forEach(key => {
    const info = clients[key];
    if (info.found) {
      assert_true(!!expected_final_client_tag,
                  `${worker_name} client tag exists`);
      assert_equals(key, expected_final_client_tag,
                    `${worker_name} client tag matches`);
      assert_equals(info.id, expected_id, `${worker_name} client id`);
      assert_equals(info.url, expected_url, `${worker_name} client url`);
      found = true;
    }
  });
  return found;
}

function check_resulting_client_ids(infos, expected_infos, actual_ids, worker) {
  assert_equals(infos.length, expected_infos.length,
                `request length for ${worker}`);
  for (var i = 0; i < infos.length; i++) {
    const tag = expected_infos[i].resultingClientIdTag;
    const url = expected_infos[i].url;
    const actual_id = infos[i].resultingClientId;
    const expected_id = actual_ids[tag];
    assert_equals(typeof(actual_id), 'string',
                  `resultingClientId for ${url} request to ${worker}`);
    if (expected_id) {
      assert_equals(actual_id, expected_id,
                    `resultingClientId for ${url} request to ${worker}`);
    } else {
      actual_ids[tag] = actual_id;
    }
  }
}

// Creates an iframe and navigates to |url|, which is expected to start a chain
// of redirects.
// - |expected_last_url| is the expected window.location after the
//   navigation.
//
// - |expected_request_infos| is the expected requests that the service workers
//   were dispatched fetch events for. The format is:
//   [
//     [
//       // Requests received by workers[0].
//       {url: url1, resultingClientIdTag: 'a'},
//       {url: url2, resultingClientIdTag: 'a'}
//     ],
//     [
//       // Requests received by workers[1].
//       {url: url3, resultingClientIdTag: 'a'}
//     ],
//     [
//       // Requests received by the cross-origin worker.
//       {url: url4, resultingClientIdTag: 'x'}
//       {url: url5, resultingClientIdTag: 'x'}
//     ]
//   ]
//   Here, |url| is |event.request.url| and |resultingClientIdTag| represents
//   |event.resultingClientId|. Since the actual client ids are not known
//   beforehand, the expectation isn't the literal expected value, but all equal
//   tags must map to the same actual id.
//
// - |expected_final_client_tag| is the resultingClientIdTag that is
//   expected to map to the created client's id. This is null if there
//   is no such tag, which can happen when the final request was a cross-origin
//   redirect to out-scope, so no worker received a fetch event whose
//   resultingClientId is the id of the resulting client.
//
// In the example above:
// - workers[0] receives two requests with the same resultingClientId.
// - workers[1] receives one request also with that resultingClientId.
// - The cross-origin worker receives two requests with the same
//   resultingClientId which differs from the previous one.
// - Assuming |expected_final_client_tag| is 'x', then the created
//   client has the id seen by the cross-origin worker above.
function redirect_test(url,
                       expected_last_url,
                       expected_request_infos,
                       expected_final_client_tag,
                       test_name) {
  promise_test(async t => {
    const frame = await with_iframe(url);
    t.add_cleanup(() => { frame.remove(); });

    // Switch on variant.
    if (document.location.search == '?client') {
      return client_variant_test(url, expected_last_url, expected_request_infos,
                                 expected_final_client_tag, test_name);
    }

    return default_variant_test(url, expected_last_url, expected_request_infos,
                                frame, test_name);
  }, test_name);
}

// The default variant tests the request interception chain and
// resulting document.location.
async function default_variant_test(url,
                                    expected_last_url,
                                    expected_request_infos,
                                    frame,
                                    test_name) {
  const expected_intercepted_urls = expected_request_infos.map(
      requests_for_worker => {
    return requests_for_worker.map(info => {
      return info.url;
    });
  });
  await check_all_intercepted_urls(expected_intercepted_urls);
  const last_url = await send_to_iframe(frame, 'getLocation');
  assert_equals(last_url, expected_last_url, 'Last URL should match.');
}

// The "client" variant tests the Clients API using resultingClientId.
async function client_variant_test(url,
                                   expected_last_url,
                                   expected_request_infos,
                                   expected_final_client_tag,
                                   test_name) {
  // Request infos is an array like:
  // [
  //   [{url: url1, resultingClientIdTag: tag1}],
  //   [{url: url2, resultingClientIdTag: tag2}],
  //   [{url: url3: resultingClientIdTag: tag3}]
  // ]
  const requestInfos = await get_all_request_infos();

  // We check the actual infos against the expected ones, and learn the
  // actual ids as we go.
  const actual_ids = {};
  check_resulting_client_ids(requestInfos[0],
                             expected_request_infos[0],
                             actual_ids,
                             'worker0');
  check_resulting_client_ids(requestInfos[1],
                             expected_request_infos[1],
                             actual_ids,
                             'worker1');
  check_resulting_client_ids(requestInfos[2],
                             expected_request_infos[2],
                             actual_ids,
                             'crossOriginWorker');

  // Now |actual_ids| maps tag to actual id:
  // {x: id1, b: id2, c: id3}
  // Ask each worker to try to resolve the actual ids to clients.
  // Only |expected_final_client_tag| should resolve to a client.
  const client_infos = await get_all_clients(actual_ids);

  // Client infos is an object like:
  // {
  //   worker0: {x: {found: true, id: id1, url: url1}, b: {found: false}},
  //   worker1: {x: {found: true, id: id1, url: url1}},
  //   crossOriginWorker: {x: {found: false}}, {b: {found: false}}
  // }
  //
  // Now check each client info. check_clients() verifies each info: only
  // |expected_final_client_tag| should ever be found and the found client
  // should have the expected url and id. A wrinkle is that not all workers
  // will find the client, if they are cross-origin to the client. This
  // means check_clients() trivially passes if no clients are found. So
  // additionally check that at least one worker found the client (|found|),
  // if that was expected (|expect_found|).
  let found = false;
  const expect_found = !!expected_final_client_tag;
  const expected_id = actual_ids[expected_final_client_tag];
  found = check_clients(client_infos.worker0,
                        expected_id,
                        expected_last_url,
                        expected_final_client_tag,
                        'worker0');
  found = check_clients(client_infos.worker1,
                        expected_id,
                        expected_last_url,
                        expected_final_client_tag,
                        'worker1') || found;
  found = check_clients(client_infos.crossOriginWorker,
                        expected_id,
                        expected_last_url,
                        expected_final_client_tag,
                        'crossOriginWorker') || found;
  assert_equals(found, expect_found, 'client found');

  if (!expect_found) {
    // TODO(falken): Ask the other origin frame if it has a client of the
    // expected URL.
  }
}

window.addEventListener('message', on_message, false);

function on_message(e) {
  if (e.origin != host_info['HTTPS_REMOTE_ORIGIN'] &&
      e.origin != host_info['HTTPS_ORIGIN'] ) {
    console.error('invalid origin: ' + e.origin);
    return;
  }
  var resolve = message_resolvers[e.data.id];
  delete message_resolvers[e.data.id];
  resolve(e.data.result);
}

function send_to_iframe(frame, message) {
  var message_id = next_message_id++;
  return new Promise(resolve => {
    message_resolvers[message_id] = resolve;
    frame.contentWindow.postMessage(
        {id: message_id, message},
        '*');
  });
}

async function get_all_clients(actual_ids) {
  const client_infos = {};
  client_infos['worker0'] = await get_clients(workers[0], actual_ids);
  client_infos['worker1'] = await get_clients(workers[1], actual_ids);
  client_infos['crossOriginWorker'] =
      await send_to_iframe(other_origin_frame,
                           {command: 'get_clients', actual_ids});
  return client_infos;
}

function get_clients(worker, actual_ids) {
  return new Promise(resolve => {
    var channel = new MessageChannel();
    channel.port1.onmessage = (msg) => {
      resolve(msg.data.clients);
    };
    worker.postMessage({command: 'getClients', actual_ids, port: channel.port2},
                       [channel.port2]);
  });
}

// Returns an array of the URLs that |worker| received fetch events for:
//   [url1, url2]
async function get_intercepted_urls(worker) {
  const infos = await get_request_infos(worker);
  return infos.map(info => { return info.url; });
}

// Returns the requests that |worker| received fetch events for. The return
// value is an array of format:
// [
//   {url: url1, resultingClientId: id},
//   {url: url2, resultingClientId: id}
// ]
function get_request_infos(worker) {
  return new Promise(resolve => {
    var channel = new MessageChannel();
    channel.port1.onmessage = (msg) => {
      resolve(msg.data.requestInfos);
    };
    worker.postMessage({command: 'getRequestInfos', port: channel.port2},
                       [channel.port2]);
  });
}

// Returns an array of the requests the workers received fetch events for:
// [
//   // Requests from workers[0].
//   [
//     {url: url1, resultingClientIdTag: tag1},
//     {url: url2, resultingClientIdTag: tag1}
//   ],
//
//   // Requests from workers[1].
//   [{url: url3, resultingClientIdTag: tag2}],
//
//   // Requests from the cross-origin worker.
//   []
// ]
async function get_all_request_infos()  {
  const request_infos = [];
  request_infos.push(await get_request_infos(workers[0]));
  request_infos.push(await get_request_infos(workers[1]));
  request_infos.push(await send_to_iframe(other_origin_frame,
                                          {command: 'get_request_infos'}));
  return request_infos;
}

let url;
let url1;
let url2;

// Normal redirect (from out-scope to in-scope).
url = SCOPE1;
redirect_test(
    OUT_SCOPE + 'url=' + encodeURIComponent(url),
    url,
    [[{url, resultingClientIdTag: 'x'}], [], []],
    'x',
    'Normal redirect to same-origin scope.');


url = SCOPE1 + '#ref';
redirect_test(
    OUT_SCOPE + 'url=' + encodeURIComponent(SCOPE1) + '#ref',
    url,
    [[{url, resultingClientIdTag: 'x'}], [], []],
    'x',
    'Normal redirect to same-origin scope with a hash fragment.');

url = SCOPE1 + '#ref2';
redirect_test(
    OUT_SCOPE + 'url=' + encodeURIComponent(url) + '#ref',
    url,
    [[{url, resultingClientIdTag: 'x'}], [], []],
    'x',
    'Normal redirect to same-origin scope with different hash fragments.');

url = OTHER_ORIGIN_SCOPE;
redirect_test(
    OUT_SCOPE + 'url=' + encodeURIComponent(url),
    url,
    [[], [], [{url, resultingClientIdTag: 'x'}]],
    'x',
    'Normal redirect to other-origin scope.');

// SW fallbacked redirect. SW doesn't handle the fetch request.
url = SCOPE1 + 'url=' + encodeURIComponent(OUT_SCOPE);
redirect_test(
    url,
    OUT_SCOPE,
    [[{url, resultingClientIdTag: 'x'}], [], []],
    'x',
    'SW-fallbacked redirect to same-origin out-scope.');

url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1);
url2 = SCOPE1;
redirect_test(
    url1,
    url2,
    [
      [
        {url: url1, resultingClientIdTag: 'x'},
        {url: url2, resultingClientIdTag: 'x'}
      ],
      [],
      []
    ],
    'x',
    'SW-fallbacked redirect to same-origin same-scope.');

url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1) + '#ref';
url2 = SCOPE1 + '#ref';
redirect_test(
    url1,
    url2,
    [
      [
        {url: url1, resultingClientIdTag: 'x'},
        {url: url2, resultingClientIdTag: 'x'}
      ],
      [],
      []
    ],
    'x',
    'SW-fallbacked redirect to same-origin same-scope with a hash fragment.');

url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE1 + '#ref2') + '#ref';
url2 = SCOPE1 + '#ref2';
redirect_test(
    url1,
    url2,
    [
      [
        {url: url1, resultingClientIdTag: 'x'},
        {url: url2, resultingClientIdTag: 'x'}
      ],
      [],
      []
    ],
    'x',
    'SW-fallbacked redirect to same-origin same-scope with different hash ' +
    'fragments.');

url1 = SCOPE1 + 'url=' + encodeURIComponent(SCOPE2);
url2 = SCOPE2;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'x'}],
      [{url: url2, resultingClientIdTag: 'x'}],
      []
    ],
    'x',
    'SW-fallbacked redirect to same-origin other-scope.');

url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
url2 = OTHER_ORIGIN_OUT_SCOPE;
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
    null,
    'SW-fallbacked redirect to other-origin out-scope.');

url1 = SCOPE1 + 'url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
url2 = OTHER_ORIGIN_SCOPE;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'a'}],
      [],
      [{url: url2, resultingClientIdTag: 'x'}]
    ],
    'x',
    'SW-fallbacked redirect to other-origin in-scope.');


url3 = SCOPE1;
url2 = OTHER_ORIGIN_SCOPE + 'url=' + encodeURIComponent(url3);
url1 = SCOPE1 + 'url=' + encodeURIComponent(url2);
redirect_test(
    url1,
    url3,
    [
      [
        {url: url1, resultingClientIdTag: 'a'},
        {url: url3, resultingClientIdTag: 'x'}
      ],
      [],
      [{url: url2, resultingClientIdTag: 'b'}]
    ],
    'x',
    'SW-fallbacked redirect to other-origin and back to same-origin.');

// SW generated redirect.
// SW: event.respondWith(Response.redirect(params['url']));
url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE);
url2 = OUT_SCOPE;
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
    'x',
    'SW-generated redirect to same-origin out-scope.');

url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE) + '#ref';
url2 = OUT_SCOPE + '#ref';
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
    'x',
    'SW-generated redirect to same-origin out-scope with a hash fragment.');

url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OUT_SCOPE + '#ref2') + '#ref';
url2 = OUT_SCOPE + '#ref2';
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
    'x',
    'SW-generated redirect to same-origin out-scope with different hash ' +
    'fragments.');

url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE1);
url2 = SCOPE1;
redirect_test(
    url1,
    url2,
    [
      [
        {url: url1, resultingClientIdTag: 'x'},
        {url: url2, resultingClientIdTag: 'x'}
      ],
      [],
      []
    ],
    'x',
    'SW-generated redirect to same-origin same-scope.');

url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(SCOPE2);
url2 = SCOPE2;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'x'}],
      [{url: url2, resultingClientIdTag: 'x'}],
      []
    ],
    'x',
    'SW-generated redirect to same-origin other-scope.');

url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
url2 = OTHER_ORIGIN_OUT_SCOPE;
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
    null,
    'SW-generated redirect to other-origin out-scope.');

url1 = SCOPE1 + 'sw=gen&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
url2 = OTHER_ORIGIN_SCOPE;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'a'}],
      [],
      [{url: url2, resultingClientIdTag: 'x'}]
    ],
    'x',
    'SW-generated redirect to other-origin in-scope.');


// SW fetched redirect.
// SW: event.respondWith(fetch(event.request));
url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OUT_SCOPE)
url2 = OUT_SCOPE;
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
    'x',
    'SW-fetched redirect to same-origin out-scope.');

url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE1);
url2 = SCOPE1;
redirect_test(
    url1,
    url2,
    [
      [
        {url: url1, resultingClientIdTag: 'x'},
        {url: url2, resultingClientIdTag: 'x'}
      ],
      [],
      []
    ],
    'x',
    'SW-fetched redirect to same-origin same-scope.');

url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(SCOPE2);
url2 = SCOPE2;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'x'}],
      [{url: url2, resultingClientIdTag: 'x'}],
      []
    ],
    'x',
    'SW-fetched redirect to same-origin other-scope.');

url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
url2 = OTHER_ORIGIN_OUT_SCOPE;
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
    null,
    'SW-fetched redirect to other-origin out-scope.');

url1 = SCOPE1 + 'sw=fetch&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
url2 = OTHER_ORIGIN_SCOPE;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'a'}],
      [],
      [{url: url2, resultingClientIdTag: 'x'}]
    ],
    'x',
    'SW-fetched redirect to other-origin in-scope.');


// SW responds with a fetch from a different url.
// SW: event.respondWith(fetch(params['url']));
url2 = SCOPE1;
url1 = SCOPE1 + 'sw=fetch-url&url=' + encodeURIComponent(url2);
redirect_test(
    url1,
    url1,
    [
      [
        {url: url1, resultingClientIdTag: 'x'}
      ],
      [],
      []
    ],
    'x',
    'SW-fetched response from different URL, same-origin same-scope.');


// Opaque redirect.
// SW: event.respondWith(fetch(
//         new Request(event.request.url, {redirect: 'manual'})));
url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OUT_SCOPE);
url2 = OUT_SCOPE;
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
    'x',
    'Redirect to same-origin out-scope with opaque redirect response.');

url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE1);
url2 = SCOPE1;
redirect_test(
    url1,
    url2,
    [
      [
        {url: url1, resultingClientIdTag: 'x'},
        {url: url2, resultingClientIdTag: 'x'}
      ],
      [],
      []
    ],
    'x',
    'Redirect to same-origin same-scope with opaque redirect response.');

url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(SCOPE2);
url2 = SCOPE2;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'x'}],
      [{url: url2, resultingClientIdTag: 'x'}],
      []
    ],
    'x',
    'Redirect to same-origin other-scope with opaque redirect response.');

url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
url2 = OTHER_ORIGIN_OUT_SCOPE;
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
    null,
    'Redirect to other-origin out-scope with opaque redirect response.');

url1 = SCOPE1 + 'sw=manual&url=' + encodeURIComponent(OTHER_ORIGIN_SCOPE);
url2 = OTHER_ORIGIN_SCOPE;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'a'}],
      [],
      [{url: url2, resultingClientIdTag: 'x'}]
    ],
    'x',
    'Redirect to other-origin in-scope with opaque redirect response.');

url= SCOPE1 + 'sw=manual&noLocationRedirect';
redirect_test(
    url, url, [[{url, resultingClientIdTag: 'x'}], [], []],
    'x',
    'No location redirect response.');


// Opaque redirect passed through Cache.
// SW responds with an opaque redirectresponse from the Cache API.
url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(OUT_SCOPE);
url2 = OUT_SCOPE;
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'x'}], [], []],
    'x',
    'Redirect to same-origin out-scope with opaque redirect response which ' +
    'is passed through Cache.');

url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE1);
url2 = SCOPE1;
redirect_test(
    url1,
    url2,
    [
      [
        {url: url1, resultingClientIdTag: 'x'},
        {url: url2, resultingClientIdTag: 'x'}
      ],
      [],
      []
    ],
    'x',
    'Redirect to same-origin same-scope with opaque redirect response which ' +
    'is passed through Cache.');

url1 = SCOPE1 + 'sw=manualThroughCache&url=' + encodeURIComponent(SCOPE2);
url2 = SCOPE2;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'x'}],
      [{url: url2, resultingClientIdTag: 'x'}],
      []
    ],
    'x',
    'Redirect to same-origin other-scope with opaque redirect response which ' +
    'is passed through Cache.');

url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
       encodeURIComponent(OTHER_ORIGIN_OUT_SCOPE);
url2 = OTHER_ORIGIN_OUT_SCOPE;
redirect_test(
    url1,
    url2,
    [[{url: url1, resultingClientIdTag: 'a'}], [], []],
    null,
    'Redirect to other-origin out-scope with opaque redirect response which ' +
    'is passed through Cache.');

url1 = SCOPE1 + 'sw=manualThroughCache&url=' +
       encodeURIComponent(OTHER_ORIGIN_SCOPE);
url2 = OTHER_ORIGIN_SCOPE;
redirect_test(
    url1,
    url2,
    [
      [{url: url1, resultingClientIdTag: 'a'}],
      [],
      [{url: url2, resultingClientIdTag: 'x'}],
    ],
    'x',
    'Redirect to other-origin in-scope with opaque redirect response which ' +
    'is passed through Cache.');

url = SCOPE1 + 'sw=manualThroughCache&noLocationRedirect';
redirect_test(
    url,
    url,
    [[{url, resultingClientIdTag: 'x'}], [], []],
    'x',
    'No location redirect response via Cache.');

// Clean up the test environment. This promise_test() needs to be the last one.
promise_test(async t => {
  registrations.forEach(async registration => {
    if (registration)
      await registration.unregister();
  });
  await send_to_iframe(other_origin_frame, {command: 'unregister'});
  other_origin_frame.remove();
}, 'clean up global state');
</script>
</body>
