1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
|
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Two soft navigations racing each other.</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script></script>
</head>
<body>
<div id="first_interaction">Click here!</div>
<div id="second_interaction">Click here!</div>
<script>
const FIRST_URL = "first-url";
const SECOND_URL = "second-url";
const button1 = document.getElementById("first_interaction");
const button2 = document.getElementById("second_interaction");
async function updateUI() {
const greeting = document.createElement("div");
greeting.textContent = "Hello, World.";
document.body.appendChild(greeting);
}
function updateUrl(t, url) {
t.state.numPushStateCalls++;
const actual_url = t.state.urlPrefix + url;
history.pushState({}, "", actual_url);
}
async function waitForSoftNavEntry(t, count = 1) {
return t.step_wait(() => t.state.softNavEntries.length >= count);
}
async function create_test(urlPrefix, callback) {
return promise_test(async (t) => {
const currentUrl = location.pathname.replace(/.*\//, "");
assert_equals(currentUrl, "racing-soft-navigations.html");
t.state = {};
t.state.urlPrefix = urlPrefix;
t.state.numPushStateCalls = 0;
t.state.softNavEntries = [];
const observer = new PerformanceObserver((list, observer) => {
// If we get two soft-navs in one observer callback...
// that is a sign that we emitted multiple for a single effect
const entries = list.getEntries();
assert_equals(entries.length, 1, "Expecting a single soft navigation");
t.state.softNavEntries.push(entries[0]);
});
observer.observe({ type: 'soft-navigation' });
// We have multiple test cases with side effects, so add some cleanup.
t.add_cleanup(async () => {
observer.disconnect();
// Go back to the original URL
for (let i = 0; i < t.state.numPushStateCalls; i++) {
history.back();
await new Promise(resolve => {
addEventListener('popstate', resolve, {once: true});
});
}
});
return callback(t);
}, "Racing multiple overlapping interactions and soft navs: " + urlPrefix);
}
async function expectationsMultipleInteractionTest(t, expected = [FIRST_URL, SECOND_URL]) {
const count = expected.length;
await t.step_wait(() => t.state.softNavEntries.length >= count, `Wait for ${count} soft navigation entries`);
// Although we await at least `count` (above), we also assert exactly `count` (here)
assert_equals(t.state.softNavEntries.length, count, `Expected ${count} soft navigation entries`);
for (let i = 0; i < count; i++) {
const entry = t.state.softNavEntries[i];
const actual_expected_url = t.state.urlPrefix + expected[i];
assert_equals(
entry.name.replace(/.*\//, ""),
actual_expected_url,
"Expect to observe the first URL change.",
);
}
}
// The following tests will trigger two interaction back to back, and each
// interaction will do a sequence of the following:
// - Triggers event listener, which schedules async work
// - updates URL
// - updates UI
// - yield, or timeout of some kind
// - Emit a soft nav entry
//
// Because there are two interactions per test, we manipulate the
// sequence of operations in various ways.
// Baseline, non overlapping interactions.
create_test("click1,url1,ui1,sn1,yield,click2,url2,ui2,sn2", async (t) => {
button1.addEventListener('click', async () => {
updateUrl(t, FIRST_URL);
updateUI();
}, { once: true });
button2.addEventListener('click', async () => {
updateUrl(t, SECOND_URL);
updateUI();
}, { once: true });
if (test_driver) {
test_driver.click(button1);
}
await waitForSoftNavEntry(t);
if(test_driver) {
test_driver.click(button2);
}
await expectationsMultipleInteractionTest(t);
});
// Both interactions start and yield (simulate network), then finish all
// required effects and emit soft nav without overlap. First interaction
// wins the "network" race.
create_test("click1,yield,click2,yield,url1,ui1,sn1,yield,url2,ui2,sn2", async (t) => {
button1.addEventListener('click', async () => {
t.step_timeout(() => {
updateUrl(t, FIRST_URL);
updateUI();
}, 0);
}, { once: true });
button2.addEventListener('click', async () => {
await waitForSoftNavEntry(t);
updateUrl(t, SECOND_URL);
updateUI();
}, { once: true });
// Start both soft navigations in rapid succession.
if (test_driver) {
test_driver.click(button1);
test_driver.click(button2);
}
await expectationsMultipleInteractionTest(t);
});
// Both interactions start and yield (simulate network), then finish all
// required effects and emit soft nav without overlap. Second interaction
// wins the "network" race.
create_test("click1,yield,click2,yield,url2,ui2,sn2,yield,url1,ui1,sn1", async (t) => {
button1.addEventListener('click', async () => {
await waitForSoftNavEntry(t);
// In this test, the first interaction sets the second URL
updateUrl(t, FIRST_URL);
updateUI();
}, { once: true });
button2.addEventListener('click', async () => {
t.step_timeout(() => {
updateUrl(t, SECOND_URL);
updateUI();
}, 0);
}, { once: true });
// Start both soft navigations in rapid succession.
if (test_driver) {
test_driver.click(button1);
test_driver.click(button2);
}
await expectationsMultipleInteractionTest(t, [SECOND_URL, FIRST_URL]);
});
// Both interactions start, immediately update URL and yield (simulate
// navigate interception), then finish all required effects later.
// Only the second URL update emits a soft nav entry.
create_test("click1,url1,yield,click2,url2,ui1,yield,ui2,sn2", async (t) => {
let first_interaction_did_finish_paint = false;
let second_interaction_did_run = false;
button1.addEventListener('click', async () => {
updateUrl(t, FIRST_URL);
await t.step_wait(() => second_interaction_did_run);
updateUI();
await new Promise(r => requestAnimationFrame(r));
await new Promise(r => t.step_timeout(r, 0));
first_interaction_did_finish_paint = true;
}, { once: true });
button2.addEventListener('click', async () => {
updateUrl(t, SECOND_URL);
second_interaction_did_run = true;
await t.step_wait(() => first_interaction_did_finish_paint);
updateUI();
}, { once: true });
// Start both soft navigations in rapid succession.
if (test_driver) {
test_driver.click(button1);
test_driver.click(button2);
}
await expectationsMultipleInteractionTest(t,[SECOND_URL]);
});
// Both interactions start, immediately update URL and yield (simulate
// navigate interception), then finish all required effects later.
// Only the second URL update emits a soft nav entry.
create_test("click1,url1,yield,click2,url2,yield,ui2,sn2,yield,ui1", async (t) => {
button1.addEventListener('click', async () => {
updateUrl(t, FIRST_URL);
await waitForSoftNavEntry(t);
updateUI();
}, { once: true });
button2.addEventListener('click', async () => {
updateUrl(t, SECOND_URL);
t.step_timeout(async () => {
updateUI();
}, 100);
}, { once: true });
// Start both soft navigations in rapid succession.
if (test_driver) {
test_driver.click(button1);
test_driver.click(button2);
}
await expectationsMultipleInteractionTest(t,[SECOND_URL]);
});
</script>
</body>
</html>
|