Rollup merge of #152194 - JonathanBrouwer:fix-rustdoc-gui, r=jieyouxu
Remove the 4 failing tests from rustdoc-gui These are the 4 tests that @Bryntet got to fail locally See [#t-infra > CI failure because `tests/rustdoc-gui/search-filter.goml`](https://rust-lang.zulipchat.com/#narrow/channel/242791-t-infra/topic/CI.20failure.20because.20.60tests.2Frustdoc-gui.2Fsearch-filter.2Egoml.60/with/572271674) r? @ghost
This commit is contained in:
commit
7afff45d57
4 changed files with 0 additions and 527 deletions
|
|
@ -1,71 +0,0 @@
|
|||
// This test check for headings text and background colors for the different themes.
|
||||
|
||||
include: "utils.goml"
|
||||
|
||||
define-function: (
|
||||
"check-colors",
|
||||
[theme, color, code_header_color, focus_background_color, headings_color],
|
||||
block {
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/struct.Foo.html"
|
||||
// This is needed so that the text color is computed.
|
||||
show-text: true
|
||||
call-function: ("switch-theme", {"theme": |theme|})
|
||||
assert-css: (
|
||||
".impl",
|
||||
{"color": |color|, "background-color": "rgba(0, 0, 0, 0)"},
|
||||
ALL,
|
||||
)
|
||||
assert-css: (
|
||||
".impl .code-header",
|
||||
{"color": |code_header_color|, "background-color": "rgba(0, 0, 0, 0)"},
|
||||
ALL,
|
||||
)
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/struct.Foo.html#impl-Foo"
|
||||
assert-css: (
|
||||
"#impl-Foo",
|
||||
{"color": |color|, "background-color": |focus_background_color|},
|
||||
)
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/struct.Foo.html#method.must_use"
|
||||
assert-css: (
|
||||
"#method\.must_use",
|
||||
{"color": |color|, "background-color": |focus_background_color|},
|
||||
ALL,
|
||||
)
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
|
||||
assert-css: (".section-header a", {"color": |color|}, ALL)
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/struct.HeavilyDocumentedStruct.html"
|
||||
// We select headings (h2, h3, h...).
|
||||
assert-css: (".docblock > :not(p) > a", {"color": |headings_color|}, ALL)
|
||||
},
|
||||
)
|
||||
|
||||
call-function: (
|
||||
"check-colors",
|
||||
{
|
||||
"theme": "ayu",
|
||||
"color": "#c5c5c5",
|
||||
"code_header_color": "#e6e1cf",
|
||||
"focus_background_color": "rgba(255, 236, 164, 0.06)",
|
||||
"headings_color": "#c5c5c5",
|
||||
},
|
||||
)
|
||||
call-function: (
|
||||
"check-colors",
|
||||
{
|
||||
"theme": "dark",
|
||||
"color": "#ddd",
|
||||
"code_header_color": "#ddd",
|
||||
"focus_background_color": "#494a3d",
|
||||
"headings_color": "#ddd",
|
||||
},
|
||||
)
|
||||
call-function: (
|
||||
"check-colors",
|
||||
{
|
||||
"theme": "light",
|
||||
"color": "black",
|
||||
"code_header_color": "black",
|
||||
"focus_background_color": "#fdffd3",
|
||||
"headings_color": "black",
|
||||
},
|
||||
)
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
// This test checks the position of the `i` for the notable traits.
|
||||
include: "utils.goml"
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/struct.NotableStructWithLongName.html"
|
||||
show-text: true
|
||||
|
||||
define-function: (
|
||||
"check-notable-tooltip-position",
|
||||
[x, i_x],
|
||||
block {
|
||||
// Checking they have the same y position.
|
||||
compare-elements-position-near: (
|
||||
"//*[@id='method.create_an_iterator_from_read']//a[normalize-space()='NotableStructWithLongName']",
|
||||
"//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']",
|
||||
{"y": 1},
|
||||
)
|
||||
// Checking they don't have the same x position.
|
||||
compare-elements-position-false: (
|
||||
"//*[@id='method.create_an_iterator_from_read']//a[normalize-space()='NotableStructWithLongName']",
|
||||
"//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']",
|
||||
["x"],
|
||||
)
|
||||
// The `i` should be *after* the type.
|
||||
assert-position: (
|
||||
"//*[@id='method.create_an_iterator_from_read']//a[normalize-space()='NotableStructWithLongName']",
|
||||
{"x": |x|},
|
||||
)
|
||||
assert-position: (
|
||||
"//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']",
|
||||
{"x": |i_x|},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
define-function: (
|
||||
"check-notable-tooltip-position-complete",
|
||||
[x, i_x, popover_x],
|
||||
block {
|
||||
call-function: ("check-notable-tooltip-position", {"x": |x|, "i_x": |i_x|})
|
||||
assert-count: ("//*[@class='tooltip popover']", 0)
|
||||
click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']"
|
||||
assert-count: ("//*[@class='tooltip popover']", 1)
|
||||
compare-elements-position-near: (
|
||||
"//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']",
|
||||
"//*[@class='tooltip popover']",
|
||||
{"y": 30}
|
||||
)
|
||||
compare-elements-position-false: (
|
||||
"//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']",
|
||||
"//*[@class='tooltip popover']",
|
||||
["x"]
|
||||
)
|
||||
assert-position: (
|
||||
"//*[@class='tooltip popover']",
|
||||
{"x": |popover_x|}
|
||||
)
|
||||
click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']"
|
||||
move-cursor-to: "//h1"
|
||||
assert-count: ("//*[@class='tooltip popover']", 0)
|
||||
},
|
||||
)
|
||||
|
||||
// We start with a wide screen.
|
||||
set-window-size: (1100, 600)
|
||||
call-function: ("check-notable-tooltip-position-complete", {
|
||||
"x": 682,
|
||||
"i_x": 960,
|
||||
"popover_x": 468,
|
||||
})
|
||||
|
||||
// Now only the `i` should be on the next line.
|
||||
set-window-size: (1055, 600)
|
||||
compare-elements-position-false: (
|
||||
"//*[@id='method.create_an_iterator_from_read']//a[normalize-space()='NotableStructWithLongName']",
|
||||
"//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']",
|
||||
["y", "x"],
|
||||
)
|
||||
|
||||
// Now both the `i` and the struct name should be on the next line.
|
||||
set-window-size: (980, 600)
|
||||
call-function: ("check-notable-tooltip-position", {
|
||||
"x": 250,
|
||||
"i_x": 528,
|
||||
})
|
||||
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/struct.NotableStructWithLongName.html"
|
||||
// This is needed to ensure that the text color is computed.
|
||||
show-text: true
|
||||
|
||||
// Now check the colors.
|
||||
define-function: (
|
||||
"check-colors",
|
||||
[theme, header_color, content_color, type_color, trait_color, link_color],
|
||||
block {
|
||||
call-function: ("switch-theme", {"theme": |theme|})
|
||||
|
||||
assert-css: (
|
||||
"//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']",
|
||||
{"color": |content_color|},
|
||||
ALL,
|
||||
)
|
||||
|
||||
move-cursor-to: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']"
|
||||
wait-for-count: (".tooltip.popover", 1)
|
||||
|
||||
assert-css: (
|
||||
"//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']",
|
||||
{"color": |link_color|},
|
||||
ALL,
|
||||
)
|
||||
|
||||
assert-css: (
|
||||
".tooltip.popover h3",
|
||||
{"color": |header_color|},
|
||||
ALL,
|
||||
)
|
||||
assert-css: (
|
||||
".tooltip.popover pre",
|
||||
{"color": |content_color|},
|
||||
ALL,
|
||||
)
|
||||
assert-css: (
|
||||
".tooltip.popover pre a.struct",
|
||||
{"color": |type_color|},
|
||||
ALL,
|
||||
)
|
||||
assert-css: (
|
||||
".tooltip.popover pre a.trait",
|
||||
{"color": |trait_color|},
|
||||
ALL,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
call-function: (
|
||||
"check-colors",
|
||||
{
|
||||
"theme": "ayu",
|
||||
"link_color": "#39afd7",
|
||||
"content_color": "#e6e1cf",
|
||||
"header_color": "#fff",
|
||||
"type_color": "#ffa0a5",
|
||||
"trait_color": "#39afd7",
|
||||
},
|
||||
)
|
||||
|
||||
call-function: (
|
||||
"check-colors",
|
||||
{
|
||||
"theme": "dark",
|
||||
"link_color": "#d2991d",
|
||||
"content_color": "#ddd",
|
||||
"header_color": "#ddd",
|
||||
"type_color": "#2dbfb8",
|
||||
"trait_color": "#b78cf2",
|
||||
},
|
||||
)
|
||||
|
||||
call-function: (
|
||||
"check-colors",
|
||||
{
|
||||
"theme": "light",
|
||||
"link_color": "#3873ad",
|
||||
"content_color": "black",
|
||||
"header_color": "black",
|
||||
"type_color": "#ad378a",
|
||||
"trait_color": "#6e4fc9",
|
||||
},
|
||||
)
|
||||
|
||||
// Checking on mobile now.
|
||||
set-window-size: (650, 600)
|
||||
wait-for-size: ("body", {"width": 650})
|
||||
call-function: ("check-notable-tooltip-position-complete", {
|
||||
"x": 26,
|
||||
"i_x": 305,
|
||||
"popover_x": 0,
|
||||
})
|
||||
|
||||
reload:
|
||||
|
||||
// Check that pressing escape works
|
||||
click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']"
|
||||
move-cursor-to: "//*[@class='tooltip popover']"
|
||||
assert-count: ("//*[@class='tooltip popover']", 1)
|
||||
press-key: "Escape"
|
||||
assert-count: ("//*[@class='tooltip popover']", 0)
|
||||
assert: "#method\.create_an_iterator_from_read .tooltip:focus"
|
||||
|
||||
// Check that clicking outside works.
|
||||
click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']"
|
||||
assert-count: ("//*[@class='tooltip popover']", 1)
|
||||
click: ".main-heading h1"
|
||||
assert-count: ("//*[@class='tooltip popover']", 0)
|
||||
assert-false: "#method\.create_an_iterator_from_read .tooltip:focus"
|
||||
|
||||
// Check that pressing tab over and over works.
|
||||
click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']"
|
||||
move-cursor-to: "//*[@class='tooltip popover']"
|
||||
assert-count: ("//*[@class='tooltip popover']", 1)
|
||||
press-key: "Tab"
|
||||
press-key: "Tab"
|
||||
press-key: "Tab"
|
||||
press-key: "Tab"
|
||||
press-key: "Tab"
|
||||
press-key: "Tab"
|
||||
press-key: "Tab"
|
||||
assert-count: ("//*[@class='tooltip popover']", 0)
|
||||
assert: "#method\.create_an_iterator_from_read .tooltip:focus"
|
||||
|
||||
define-function: (
|
||||
"setup-popup",
|
||||
[],
|
||||
block {
|
||||
store-window-property: {"scrollY": scroll}
|
||||
click: "#method\.create_an_iterator_from_read .fn"
|
||||
// We ensure that the scroll position changed.
|
||||
assert-window-property-false: {"scrollY": |scroll|}
|
||||
// Store the new position.
|
||||
store-window-property: {"scrollY": scroll}
|
||||
click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']"
|
||||
wait-for: "//*[@class='tooltip popover']"
|
||||
click: ".main-heading h1"
|
||||
}
|
||||
)
|
||||
|
||||
// Now we check that the focus isn't given back to the wrong item when opening
|
||||
// another popover.
|
||||
call-function: ("setup-popup", {})
|
||||
click: ".main-heading h1"
|
||||
// We ensure we didn't come back to the previous focused item.
|
||||
assert-window-property-false: {"scrollY": |scroll|}
|
||||
|
||||
// Same but with Escape handling.
|
||||
call-function: ("setup-popup", {})
|
||||
press-key: "Escape"
|
||||
// We ensure we didn't come back to the previous focused item.
|
||||
assert-window-property-false: {"scrollY": |scroll|}
|
||||
|
||||
// Opening the mobile sidebar should close the popover.
|
||||
set-window-size: (650, 600)
|
||||
click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']"
|
||||
assert-count: ("//*[@class='tooltip popover']", 1)
|
||||
click: ".sidebar-menu-toggle"
|
||||
assert: "//*[@class='sidebar shown']"
|
||||
assert-count: ("//*[@class='tooltip popover']", 0)
|
||||
assert-false: "#method\.create_an_iterator_from_read .tooltip:focus"
|
||||
|
||||
// Also check the focus handling for the settings button.
|
||||
set-window-size: (1100, 600)
|
||||
reload:
|
||||
assert-count: ("//*[@class='tooltip popover']", 0)
|
||||
click: "//*[@id='method.create_an_iterator_from_read']//*[@class='tooltip']"
|
||||
wait-for-count: ("//*[@class='tooltip popover']", 1)
|
||||
call-function: ("open-settings-menu", {})
|
||||
wait-for-count: ("//*[@class='tooltip popover']", 0)
|
||||
assert-false: "#method\.create_an_iterator_from_read .tooltip:focus"
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
// Checks that the crate search filtering is handled correctly and changes the results.
|
||||
include: "utils.goml"
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
|
||||
show-text: true
|
||||
call-function: ("perform-search", {"query": "test"})
|
||||
assert-text: ("#results .externcrate", "test_docs")
|
||||
|
||||
wait-for: "#crate-search"
|
||||
// We now want to change the crate filter.
|
||||
click: "#crate-search"
|
||||
// We select "lib2" option then press enter to change the filter.
|
||||
press-key: "ArrowDown"
|
||||
press-key: "ArrowDown"
|
||||
press-key: "ArrowDown"
|
||||
press-key: "ArrowDown"
|
||||
press-key: "ArrowDown"
|
||||
press-key: "Enter"
|
||||
// Waiting for the search results to appear...
|
||||
wait-for: "#search-tabs"
|
||||
wait-for-false: "#search-tabs .count.loading"
|
||||
assert-document-property: ({"URL": "&filter-crate="}, CONTAINS)
|
||||
// We check that there is no more "test_docs" appearing.
|
||||
assert-false: "#results .externcrate"
|
||||
// We also check that "lib2" is the filter crate.
|
||||
assert-property: ("#crate-search", {"value": "lib2"})
|
||||
|
||||
// Now we check that leaving the search results and putting them back keeps the
|
||||
// crate filtering.
|
||||
press-key: "Escape"
|
||||
wait-for-css: ("#main-content", {"display": "block"})
|
||||
click: "#search-button"
|
||||
wait-for: ".search-input"
|
||||
wait-for-css: ("#main-content", {"display": "none"})
|
||||
// We check that there is no more "test_docs" appearing.
|
||||
assert-false: "#results .externcrate"
|
||||
assert-property: ("#crate-search", {"value": "lib2"})
|
||||
|
||||
// Selecting back "All crates"
|
||||
click: "#crate-search"
|
||||
press-key: "ArrowUp"
|
||||
press-key: "ArrowUp"
|
||||
press-key: "ArrowUp"
|
||||
press-key: "ArrowUp"
|
||||
press-key: "ArrowUp"
|
||||
press-key: "Enter"
|
||||
// Waiting for the search results to appear...
|
||||
wait-for: "#search-tabs"
|
||||
wait-for-false: "#search-tabs .count.loading"
|
||||
assert-property: ("#crate-search", {"value": "all crates"})
|
||||
|
||||
// Checking that the URL parameter is taken into account for crate filtering.
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=test&filter-crate=lib2"
|
||||
wait-for: "#crate-search"
|
||||
assert-property: ("#crate-search", {"value": "lib2"})
|
||||
assert-false: "#results .externcrate"
|
||||
|
||||
// Checking that the text for the "title" is correct (the "all crates" comes from the "<select>").
|
||||
assert-text: (".search-switcher", "Search results in all crates", STARTS_WITH)
|
||||
|
||||
// Checking the display of the crate filter.
|
||||
// We start with the light theme.
|
||||
call-function: ("switch-theme", {"theme": "light"})
|
||||
|
||||
set-timeout: 2000
|
||||
wait-for: "#crate-search"
|
||||
assert-css: ("#crate-search", {
|
||||
"border": "1px solid #e0e0e0",
|
||||
"color": "black",
|
||||
"background-color": "white",
|
||||
})
|
||||
|
||||
// We now check the dark theme.
|
||||
call-function: ("switch-theme", {"theme": "dark"})
|
||||
wait-for-css: ("#crate-search", {
|
||||
"border": "1px solid #e0e0e0",
|
||||
"color": "#ddd",
|
||||
"background-color": "#353535",
|
||||
})
|
||||
|
||||
// And finally we check the ayu theme.
|
||||
call-function: ("switch-theme", {"theme": "ayu"})
|
||||
wait-for-css: ("#crate-search", {
|
||||
"border": "1px solid #5c6773",
|
||||
"color": "#c5c5c5",
|
||||
"background-color": "#0f1419",
|
||||
})
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
// ignore-tidy-linelength
|
||||
// Checks that the search results have the expected width.
|
||||
include: "utils.goml"
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
|
||||
set-window-size: (900, 1000)
|
||||
write-into: (".search-input", "test")
|
||||
// To be SURE that the search will be run.
|
||||
press-key: 'Enter'
|
||||
wait-for: "#crate-search"
|
||||
wait-for-false: "#search-tabs .count.loading"
|
||||
// The width is returned by "getComputedStyle" which returns the exact number instead of the
|
||||
// CSS rule which is "50%"...
|
||||
assert-size: (".search-results div.desc", {"width": 248})
|
||||
store-size: (".search-results .result-name .typename", {"width": width})
|
||||
set-window-size: (600, 100)
|
||||
// As counter-intuitive as it may seem, in this width, the width is "100%", which is why
|
||||
// when computed it's larger.
|
||||
assert-size: (".search-results div.desc", {"width": 566})
|
||||
|
||||
// The result set is all on one line.
|
||||
compare-elements-position-near: (
|
||||
".search-results .result-name .typename",
|
||||
".search-results .result-name .path",
|
||||
{"y": 2},
|
||||
)
|
||||
compare-elements-position-near-false: (
|
||||
".search-results .result-name .typename",
|
||||
".search-results .result-name .path",
|
||||
{"x": 5},
|
||||
)
|
||||
// The width of the "typename" isn't fixed anymore in this display mode.
|
||||
store-size: (".search-results .result-name .typename", {"width": new_width})
|
||||
assert: |new_width| < |width| - 10
|
||||
|
||||
// Check that if the search is too long on mobile, it'll go under the "typename".
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html?search=SuperIncrediblyLongLongLongLongLongLongLongGigaGigaGigaMegaLongLongLongStructName"
|
||||
wait-for: "#crate-search"
|
||||
wait-for-false: "#search-tabs .count.loading"
|
||||
compare-elements-position-near: (
|
||||
".search-results .result-name .typename",
|
||||
".search-results .result-name .path",
|
||||
{"y": 2, "x": 0},
|
||||
)
|
||||
compare-elements-size-near: (
|
||||
".search-results .result-name",
|
||||
".search-results .result-name .path",
|
||||
{"width": 8, "height": 8},
|
||||
)
|
||||
|
||||
// Check that the crate filter `<select>` is correctly handled when it goes to next line.
|
||||
// To do so we need to update the length of one of its `<option>`.
|
||||
set-window-size: (900, 900)
|
||||
|
||||
// First we check the current width, height and position.
|
||||
assert-css: ("#crate-search", {"width": "159px"})
|
||||
store-size: (".search-switcher", {
|
||||
"height": search_results_title_height,
|
||||
"width": search_results_title_width,
|
||||
})
|
||||
assert-css: ("#search", {"width": "640px"})
|
||||
|
||||
// Then we update the text of one of the `<option>`.
|
||||
set-text: (
|
||||
"#crate-search option",
|
||||
"sdjfaksdjfaksjdbfkadsbfkjsadbfkdsbkfbsadkjfbkdsabfkadsfkjdsafa",
|
||||
)
|
||||
|
||||
// Then we compare again to confirm the height didn't change.
|
||||
assert-size: ("#crate-search", {"width": 185})
|
||||
assert-size: (".search-switcher", {
|
||||
"height": |search_results_title_height|,
|
||||
})
|
||||
assert-css: ("#search", {"width": "640px"})
|
||||
assert: |search_results_title_width| <= 640
|
||||
|
||||
// Now checking that the crate filter is working as expected too.
|
||||
show-text: true
|
||||
define-function: (
|
||||
"check-filter",
|
||||
[theme, border, filter, hover_border, hover_filter],
|
||||
block {
|
||||
call-function: ("switch-theme", {"theme": |theme|})
|
||||
wait-for: "#crate-search"
|
||||
wait-for-false: "#search-tabs .count.loading"
|
||||
assert-css: ("#crate-search", {"border": "1px solid " + |border|})
|
||||
assert-css: ("#crate-search-div::after", {"filter": |filter|})
|
||||
move-cursor-to: "#crate-search"
|
||||
assert-css: ("#crate-search", {"border": "1px solid " + |hover_border|})
|
||||
assert-css: ("#crate-search-div::after", {"filter": |hover_filter|})
|
||||
move-cursor-to: ".search-input"
|
||||
},
|
||||
)
|
||||
|
||||
call-function: ("check-filter", {
|
||||
"theme": "ayu",
|
||||
"border": "#5c6773",
|
||||
"filter": "invert(0.41) sepia(0.12) saturate(4.87) hue-rotate(171deg) brightness(0.94) contrast(0.94)",
|
||||
"hover_border": "#e0e0e0",
|
||||
"hover_filter": "invert(0.98) sepia(0.12) saturate(0.81) hue-rotate(343deg) brightness(1.13) contrast(0.76)",
|
||||
})
|
||||
call-function: ("check-filter", {
|
||||
"theme": "dark",
|
||||
"border": "#e0e0e0",
|
||||
"filter": "invert(0.94) sepia(0) saturate(7.21) hue-rotate(255deg) brightness(0.9) contrast(0.9)",
|
||||
"hover_border": "#2196f3",
|
||||
"hover_filter": "invert(0.69) sepia(0.6) saturate(66.13) hue-rotate(184deg) brightness(1) contrast(0.91)",
|
||||
})
|
||||
call-function: ("check-filter", {
|
||||
"theme": "light",
|
||||
"border": "#e0e0e0",
|
||||
"filter": "invert(1) sepia(0) saturate(42.23) hue-rotate(289deg) brightness(1.14) contrast(0.76)",
|
||||
"hover_border": "#717171",
|
||||
"hover_filter": "invert(0.44) sepia(0.18) saturate(0.23) hue-rotate(317deg) brightness(0.96) contrast(0.93)",
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue