From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- accessible/tests/browser/scroll/browser.ini | 15 + .../tests/browser/scroll/browser_test_scrollTo.js | 36 ++ .../browser/scroll/browser_test_scroll_bounds.js | 606 +++++++++++++++++++++ .../scroll/browser_test_scroll_substring.js | 67 +++ .../tests/browser/scroll/browser_test_zoom_text.js | 145 +++++ accessible/tests/browser/scroll/head.js | 18 + 6 files changed, 887 insertions(+) create mode 100644 accessible/tests/browser/scroll/browser.ini create mode 100644 accessible/tests/browser/scroll/browser_test_scrollTo.js create mode 100644 accessible/tests/browser/scroll/browser_test_scroll_bounds.js create mode 100644 accessible/tests/browser/scroll/browser_test_scroll_substring.js create mode 100644 accessible/tests/browser/scroll/browser_test_zoom_text.js create mode 100644 accessible/tests/browser/scroll/head.js (limited to 'accessible/tests/browser/scroll') diff --git a/accessible/tests/browser/scroll/browser.ini b/accessible/tests/browser/scroll/browser.ini new file mode 100644 index 0000000000..0cae6a7c0e --- /dev/null +++ b/accessible/tests/browser/scroll/browser.ini @@ -0,0 +1,15 @@ +[DEFAULT] +subsuite = a11y +support-files = + head.js + !/accessible/tests/browser/shared-head.js + !/accessible/tests/browser/*.jsm + !/accessible/tests/mochitest/*.js +prefs = + javascript.options.asyncstack_capture_debuggee_only=false + +[browser_test_zoom_text.js] +skip-if = os == 'win' # bug 1372296 +[browser_test_scroll_bounds.js] +[browser_test_scrollTo.js] +[browser_test_scroll_substring.js] diff --git a/accessible/tests/browser/scroll/browser_test_scrollTo.js b/accessible/tests/browser/scroll/browser_test_scrollTo.js new file mode 100644 index 0000000000..43a230b7b8 --- /dev/null +++ b/accessible/tests/browser/scroll/browser_test_scrollTo.js @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test nsIAccessible::scrollTo. + */ +addAccessibleTask( + ` +
+

a

+

b

+
+ `, + async function (browser, docAcc) { + const scroller = findAccessibleChildByID(docAcc, "scroller"); + // scroller can only fit one of p1 or p2, not both. + // p1 is on screen already. + const p2 = findAccessibleChildByID(docAcc, "p2"); + info("scrollTo p2"); + let scrolled = waitForEvent( + nsIAccessibleEvent.EVENT_SCROLLING_END, + scroller + ); + p2.scrollTo(SCROLL_TYPE_ANYWHERE); + await scrolled; + const p1 = findAccessibleChildByID(docAcc, "p1"); + info("scrollTo p1"); + scrolled = waitForEvent(nsIAccessibleEvent.EVENT_SCROLLING_END, scroller); + p1.scrollTo(SCROLL_TYPE_ANYWHERE); + await scrolled; + }, + { topLevel: true, iframe: true, remoteIframe: true, chrome: true } +); diff --git a/accessible/tests/browser/scroll/browser_test_scroll_bounds.js b/accessible/tests/browser/scroll/browser_test_scroll_bounds.js new file mode 100644 index 0000000000..bd61340aa6 --- /dev/null +++ b/accessible/tests/browser/scroll/browser_test_scroll_bounds.js @@ -0,0 +1,606 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts( + { name: "layout.js", dir: MOCHITESTS_DIR }, + { name: "role.js", dir: MOCHITESTS_DIR } +); +requestLongerTimeout(2); + +const appUnitsPerDevPixel = 60; + +function testCachedScrollPosition(acc, expectedX, expectedY) { + let cachedPosition = ""; + try { + cachedPosition = acc.cache.getStringProperty("scroll-position"); + } catch (e) { + // If the key doesn't exist, this means 0, 0. + cachedPosition = "0, 0"; + } + + // The value we retrieve from the cache is in app units, but the values + // passed in are in pixels. Since the retrieved value is a string, + // and harder to modify, adjust our expected x and y values to match its units. + return ( + cachedPosition == + `${expectedX * appUnitsPerDevPixel}, ${expectedY * appUnitsPerDevPixel}` + ); +} + +function getCachedBounds(acc) { + let cachedBounds = ""; + try { + cachedBounds = acc.cache.getStringProperty("relative-bounds"); + } catch (e) { + ok(false, "Unable to fetch cached bounds from cache!"); + } + return cachedBounds; +} + +/** + * Test bounds of accessibles after scrolling + */ +addAccessibleTask( + ` +
+
+ +
+
+ `, + async function (browser, docAcc) { + ok(docAcc, "iframe document acc is present"); + await testBoundsWithContent(docAcc, "square", browser); + await testBoundsWithContent(docAcc, "rect", browser); + + await invokeContentTask(browser, [], () => { + content.document.getElementById("square").scrollIntoView(); + }); + + await waitForContentPaint(browser); + + await testBoundsWithContent(docAcc, "square", browser); + await testBoundsWithContent(docAcc, "rect", browser); + + // Scroll rect into view, but also make it reflow so we can be sure the + // bounds are correct for reflowed frames. + await invokeContentTask(browser, [], () => { + const rect = content.document.getElementById("rect"); + rect.scrollIntoView(); + rect.style.width = "300px"; + rect.offsetTop; // Flush layout. + rect.style.width = "200px"; + rect.offsetTop; // Flush layout. + }); + + await waitForContentPaint(browser); + await testBoundsWithContent(docAcc, "square", browser); + await testBoundsWithContent(docAcc, "rect", browser); + }, + { iframe: true, remoteIframe: true, chrome: true } +); + +/** + * Test scroll offset on cached accessibles + */ +addAccessibleTask( + ` +
+
+ +
+
+ `, + async function (browser, docAcc) { + ok(docAcc, "iframe document acc is present"); + await untilCacheOk( + () => testCachedScrollPosition(docAcc, 0, 0), + "Correct initial scroll position." + ); + const rectAcc = findAccessibleChildByID(docAcc, "rect"); + const rectInitialBounds = getCachedBounds(rectAcc); + + await invokeContentTask(browser, [], () => { + content.document.getElementById("square").scrollIntoView(); + }); + + await waitForContentPaint(browser); + + // The only content to scroll over is `square`'s top margin + // so our scroll offset here should be 3000px + await untilCacheOk( + () => testCachedScrollPosition(docAcc, 0, 3000), + "Correct scroll position after first scroll." + ); + + // Scroll rect into view, but also make it reflow so we can be sure the + // bounds are correct for reflowed frames. + await invokeContentTask(browser, [], () => { + const rect = content.document.getElementById("rect"); + rect.scrollIntoView(); + rect.style.width = "300px"; + rect.offsetTop; + rect.style.width = "200px"; + }); + + await waitForContentPaint(browser); + // We have to scroll over `square`'s top margin (3000px), + // `square` itself (100px), and `square`'s bottom margin (4000px). + // This should give us a 7100px offset. + await untilCacheOk( + () => testCachedScrollPosition(docAcc, 0, 7100), + "Correct final scroll position." + ); + await untilCacheIs( + () => getCachedBounds(rectAcc), + rectInitialBounds, + "Cached relative bounds don't change when scrolling" + ); + }, + { iframe: true, remoteIframe: true } +); + +/** + * Test scroll offset fixed-pos acc accs + */ +addAccessibleTask( + ` +
+
+ +
+
+ `, + async function (browser, docAcc) { + const origTopBounds = await testBoundsWithContent(docAcc, "top", browser); + const origDBounds = await testBoundsWithContent(docAcc, "d", browser); + const e = waitForEvent(EVENT_REORDER, docAcc); + await invokeContentTask(browser, [], () => { + for (let i = 0; i < 1000; ++i) { + const div = content.document.createElement("div"); + div.innerHTML = ""; + content.document.body.append(div); + } + }); + await e; + + await invokeContentTask(browser, [], () => { + // scroll to the bottom of the page + content.window.scrollTo(0, content.document.body.scrollHeight); + }); + + await waitForContentPaint(browser); + + let newTopBounds = await testBoundsWithContent(docAcc, "top", browser); + let newDBounds = await testBoundsWithContent(docAcc, "d", browser); + is( + origTopBounds[0], + newTopBounds[0], + "x of fixed elem is unaffected by scrolling" + ); + is( + origTopBounds[1], + newTopBounds[1], + "y of fixed elem is unaffected by scrolling" + ); + is( + origTopBounds[2], + newTopBounds[2], + "width of fixed elem is unaffected by scrolling" + ); + is( + origTopBounds[3], + newTopBounds[3], + "height of fixed elem is unaffected by scrolling" + ); + is( + origDBounds[0], + newTopBounds[0], + "x of fixed elem container is unaffected by scrolling" + ); + is( + origDBounds[1], + newDBounds[1], + "y of fixed elem container is unaffected by scrolling" + ); + is( + origDBounds[2], + newDBounds[2], + "width of fixed container elem is unaffected by scrolling" + ); + is( + origDBounds[3], + newDBounds[3], + "height of fixed container elem is unaffected by scrolling" + ); + + await invokeContentTask(browser, [], () => { + // remove position styling + content.document.getElementById("d").style = ""; + }); + + await waitForContentPaint(browser); + + newTopBounds = await testBoundsWithContent(docAcc, "top", browser); + newDBounds = await testBoundsWithContent(docAcc, "d", browser); + is( + origTopBounds[0], + newTopBounds[0], + "x of non-fixed element remains accurate." + ); + ok(newTopBounds[1] < 0, "y coordinate shows item scrolled off page"); + is( + origTopBounds[2], + newTopBounds[2], + "width of non-fixed element remains accurate." + ); + is( + origTopBounds[3], + newTopBounds[3], + "height of non-fixed element remains accurate." + ); + is( + origDBounds[0], + newDBounds[0], + "x of non-fixed container element remains accurate." + ); + ok(newDBounds[1] < 0, "y coordinate shows container scrolled off page"); + // Removing the position styling on this acc causes it to be bound by + // its parent's bounding box, which alters its width as a block element. + // We don't particularly care about width in this test, so skip it. + is( + origDBounds[3], + newDBounds[3], + "height of non-fixed container element remains accurate." + ); + + await invokeContentTask(browser, [], () => { + // re-add position styling + content.document.getElementById("d").style = "position:fixed;"; + }); + + await waitForContentPaint(browser); + + newTopBounds = await testBoundsWithContent(docAcc, "top", browser); + newDBounds = await testBoundsWithContent(docAcc, "d", browser); + is( + origTopBounds[0], + newTopBounds[0], + "x correct when position:fixed is added." + ); + is( + origTopBounds[1], + newTopBounds[1], + "y correct when position:fixed is added." + ); + is( + origTopBounds[2], + newTopBounds[2], + "width correct when position:fixed is added." + ); + is( + origTopBounds[3], + newTopBounds[3], + "height correct when position:fixed is added." + ); + is( + origDBounds[0], + newDBounds[0], + "x of container correct when position:fixed is added." + ); + is( + origDBounds[1], + newDBounds[1], + "y of container correct when position:fixed is added." + ); + is( + origDBounds[2], + newDBounds[2], + "width of container correct when position:fixed is added." + ); + is( + origDBounds[3], + newDBounds[3], + "height of container correct when position:fixed is added." + ); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +/** + * Test position: fixed for containers that would otherwise be pruned from the + * a11y tree. + */ +addAccessibleTask( + ` + + + + +
+

bottom

+ `, + async function (browser, docAcc) { + const fixed = findAccessibleChildByID(docAcc, "fixed"); + ok(fixed, "fixed is accessible"); + isnot(fixed.role, ROLE_TABLE, "fixed doesn't have ROLE_TABLE"); + ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible"); + info("Setting position: fixed on mutate"); + let shown = waitForEvent(EVENT_SHOW, "mutate"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("mutate").style.position = "fixed"; + }); + await shown; + const origFixedBounds = await testBoundsWithContent( + docAcc, + "fixed", + browser + ); + const origMutateBounds = await testBoundsWithContent( + docAcc, + "mutate", + browser + ); + info("Scrolling to bottom of page"); + await invokeContentTask(browser, [], () => { + content.window.scrollTo(0, content.document.body.scrollHeight); + }); + await waitForContentPaint(browser); + const newFixedBounds = await testBoundsWithContent( + docAcc, + "fixed", + browser + ); + Assert.deepEqual( + newFixedBounds, + origFixedBounds, + "fixed bounds are unchanged" + ); + const newMutateBounds = await testBoundsWithContent( + docAcc, + "mutate", + browser + ); + Assert.deepEqual( + newMutateBounds, + origMutateBounds, + "mutate bounds are unchanged" + ); + }, + { chrome: true, iframe: true, remoteIframe: true } +); + +/** + * Test scroll offset on sticky-pos acc + */ +addAccessibleTask( + ` +
+ +
+ `, + async function (browser, docAcc) { + const containerBounds = await testBoundsWithContent(docAcc, "d", browser); + const e = waitForEvent(EVENT_REORDER, docAcc); + await invokeContentTask(browser, [], () => { + for (let i = 0; i < 1000; ++i) { + const div = content.document.createElement("div"); + div.innerHTML = ""; + content.document.body.append(div); + } + }); + await e; + for (let id of ["d", "top"]) { + info(`Verifying bounds for acc with ID ${id}`); + const origBounds = await testBoundsWithContent(docAcc, id, browser); + + info("Scrolling partially"); + await invokeContentTask(browser, [], () => { + // scroll some of the window + content.window.scrollTo(0, 50); + }); + + await waitForContentPaint(browser); + + let newBounds = await testBoundsWithContent(docAcc, id, browser); + is( + origBounds[0], + newBounds[0], + `x coord of sticky element is unaffected by scrolling` + ); + ok( + origBounds[1] > newBounds[1] && newBounds[1] >= 0, + "sticky element scrolled, but not off the page" + ); + is( + origBounds[2], + newBounds[2], + `width of sticky element is unaffected by scrolling` + ); + is( + origBounds[3], + newBounds[3], + `height of sticky element is unaffected by scrolling` + ); + + info("Scrolling to bottom"); + await invokeContentTask(browser, [], () => { + // scroll to the bottom of the page + content.window.scrollTo(0, content.document.body.scrollHeight); + }); + + await waitForContentPaint(browser); + + newBounds = await testBoundsWithContent(docAcc, id, browser); + is( + origBounds[0], + newBounds[0], + `x coord of sticky element is unaffected by scrolling` + ); + // Subtract margin from container screen coords to get chrome height + // which is where our y pos should be + is( + newBounds[1], + containerBounds[1] - 100, + "Sticky element is top of screen" + ); + is( + origBounds[2], + newBounds[2], + `width of sticky element is unaffected by scrolling` + ); + is( + origBounds[3], + newBounds[3], + `height of sticky element is unaffected by scrolling` + ); + + info("Removing position style on container"); + await invokeContentTask(browser, [], () => { + // remove position styling + content.document.getElementById("d").style = + "margin-top: 100px; margin-left: 75px;"; + }); + + await waitForContentPaint(browser); + + newBounds = await testBoundsWithContent(docAcc, id, browser); + + is( + origBounds[0], + newBounds[0], + `x coord of non-sticky element remains accurate.` + ); + ok(newBounds[1] < 0, "y coordinate shows item scrolled off page"); + + // Removing the position styling on this acc causes it to be bound by + // its parent's bounding box, which alters its width as a block element. + // We don't particularly care about width in this test, so skip it. + is( + origBounds[3], + newBounds[3], + `height of non-sticky element remains accurate.` + ); + + info("Adding position style on container"); + await invokeContentTask(browser, [], () => { + // re-add position styling + content.document.getElementById("d").style = + "margin-top: 100px; margin-left: 75px; position:sticky; top:0px;"; + }); + + await waitForContentPaint(browser); + + newBounds = await testBoundsWithContent(docAcc, id, browser); + is( + origBounds[0], + newBounds[0], + `x coord of sticky element is unaffected by scrolling` + ); + is( + newBounds[1], + containerBounds[1] - 100, + "Sticky element is top of screen" + ); + is( + origBounds[2], + newBounds[2], + `width of sticky element is unaffected by scrolling` + ); + is( + origBounds[3], + newBounds[3], + `height of sticky element is unaffected by scrolling` + ); + + info("Scrolling back up to test next ID"); + await invokeContentTask(browser, [], () => { + // scroll some of the window + content.window.scrollTo(0, 0); + }); + } + }, + { chrome: false, iframe: false, remoteIframe: false } +); + +/** + * Test position: sticky for containers that would otherwise be pruned from the + * a11y tree. + */ +addAccessibleTask( + ` +
+
+ +
+

stickyEnd

+
+
+ +
+

mutateEnd

+
+ `, + async function (browser, docAcc) { + ok(findAccessibleChildByID(docAcc, "sticky"), "sticky is accessible"); + info("Scrolling to sticky"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("sticky").scrollIntoView(); + }); + await waitForContentPaint(browser); + const origStickyBounds = await testBoundsWithContent( + docAcc, + "sticky", + browser + ); + info("Scrolling to stickyEnd"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("stickyEnd").scrollIntoView(); + }); + await waitForContentPaint(browser); + const newStickyBounds = await testBoundsWithContent( + docAcc, + "sticky", + browser + ); + Assert.deepEqual( + newStickyBounds, + origStickyBounds, + "sticky bounds are unchanged" + ); + + ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible"); + info("Setting position: sticky on mutate"); + let shown = waitForEvent(EVENT_SHOW, "mutate"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("mutate").style.position = "sticky"; + }); + await shown; + info("Scrolling to mutate"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("mutate").scrollIntoView(); + }); + await waitForContentPaint(browser); + const origMutateBounds = await testBoundsWithContent( + docAcc, + "mutate", + browser + ); + info("Scrolling to mutateEnd"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("mutateEnd").scrollIntoView(); + }); + await waitForContentPaint(browser); + const newMutateBounds = await testBoundsWithContent( + docAcc, + "mutate", + browser + ); + assertBoundsFuzzyEqual(newMutateBounds, origMutateBounds); + }, + { chrome: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/scroll/browser_test_scroll_substring.js b/accessible/tests/browser/scroll/browser_test_scroll_substring.js new file mode 100644 index 0000000000..e8426d00ca --- /dev/null +++ b/accessible/tests/browser/scroll/browser_test_scroll_substring.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/layout.js */ +loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR }); + +/** + * Test nsIAccessibleText::scrollSubstringTo. + */ +addAccessibleTask( + ` + +
+
+
+
+
+
+It's a jetpack, Michael. What could possibly go wrong?
+
+
+
+
+
+The only thing I found in the fridge was a dead dove in a bag.
+
`, + async function (browser, docAcc) { + let text = findAccessibleChildByID(docAcc, "text", [nsIAccessibleText]); + let [, containerY, , containerHeight] = getBounds(text); + let getCharY = () => { + let objY = {}; + text.getCharacterExtents(7, {}, objY, {}, {}, COORDTYPE_SCREEN_RELATIVE); + return objY.value; + }; + ok( + containerHeight < getCharY(), + "Character is outside of container bounds" + ); + text.scrollSubstringTo(7, 8, SCROLL_TYPE_TOP_EDGE); + + await waitForContentPaint(browser); + await untilCacheIs( + getCharY, + containerY, + "Character is scrolled to top of container" + ); + }, + { + topLevel: true, + iframe: true, + remoteIframe: true, + chrome: true, + } +); diff --git a/accessible/tests/browser/scroll/browser_test_zoom_text.js b/accessible/tests/browser/scroll/browser_test_zoom_text.js new file mode 100644 index 0000000000..4fc0a56b43 --- /dev/null +++ b/accessible/tests/browser/scroll/browser_test_zoom_text.js @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/layout.js */ +loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR }); + +async function runTests(browser, accDoc) { + await loadContentScripts(browser, { + script: "Layout.sys.mjs", + symbol: "Layout", + }); + + let paragraph = findAccessibleChildByID(accDoc, "paragraph", [ + nsIAccessibleText, + ]); + let offset = 64; // beginning of 4th stanza + + let [x /* ,y*/] = getPos(paragraph); + let [docX, docY] = getPos(accDoc); + + paragraph.scrollSubstringToPoint( + offset, + offset, + COORDTYPE_SCREEN_RELATIVE, + docX, + docY + ); + + await waitForContentPaint(browser); + testTextPos(paragraph, offset, [x, docY], COORDTYPE_SCREEN_RELATIVE); + + await SpecialPowers.spawn(browser, [], () => { + content.Layout.zoomDocument(content.document, 2.0); + }); + + paragraph = findAccessibleChildByID(accDoc, "paragraph2", [ + nsIAccessibleText, + ]); + offset = 52; // // beginning of 4th stanza + [x /* ,y*/] = getPos(paragraph); + paragraph.scrollSubstringToPoint( + offset, + offset, + COORDTYPE_SCREEN_RELATIVE, + docX, + docY + ); + + await waitForContentPaint(browser); + testTextPos(paragraph, offset, [x, docY], COORDTYPE_SCREEN_RELATIVE); +} + +/** + * Test caching of accessible object states + */ +addAccessibleTask( + ` +









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+










+

+ Пошел котик на торжок
+ Купил котик пирожок
+ Пошел котик на улочку
+ Купил котик булочку
+

+










+









+









+









+









+










+

+ Самому ли съесть
+ Либо Сашеньке снесть
+ Я и сам укушу
+ Я и Сашеньке снесу
+

+










+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









`, + runTests +); diff --git a/accessible/tests/browser/scroll/head.js b/accessible/tests/browser/scroll/head.js new file mode 100644 index 0000000000..afc50984bd --- /dev/null +++ b/accessible/tests/browser/scroll/head.js @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); -- cgit v1.2.3