diff --git a/src/core/plugins/deep-linking/helpers.js b/src/core/plugins/deep-linking/helpers.js index d06bbe27dc1..ccfef08d8d0 100644 --- a/src/core/plugins/deep-linking/helpers.js +++ b/src/core/plugins/deep-linking/helpers.js @@ -1,6 +1,11 @@ export const setHash = (value) => { if(value) { - return history.pushState(null, null, `#${value}`) + // Include the current pathname and search explicitly so that a + // document-level `` (set by frameworks like Angular) does not + // cause `pushState` to resolve `#value` against the base URL and replace + // the current route. See https://github.com/swagger-api/swagger-ui/issues/10591 + const { pathname, search } = window.location + return history.pushState(null, null, `${pathname}${search}#${value}`) } else { return window.location.hash = "" } diff --git a/test/e2e-cypress/e2e/bugs/10591.cy.js b/test/e2e-cypress/e2e/bugs/10591.cy.js new file mode 100644 index 00000000000..3b946027315 --- /dev/null +++ b/test/e2e-cypress/e2e/bugs/10591.cy.js @@ -0,0 +1,33 @@ +describe("#10591: deep-linking with document should not replace current route", () => { + const baseUrl = "/?deepLinking=true&url=/documents/features/deep-linking.openapi.yaml" + + it("should preserve the page pathname and search when an operation is expanded", () => { + cy.intercept( + { + method: "GET", + pathname: "/", + }, + (req) => { + req.continue((res) => { + if (typeof res.body === "string" && res.body.includes("")) { + res.body = res.body.replace("", "\n") + } + }) + } + ).as("indexHtml") + + cy.visit(baseUrl) + cy.wait("@indexHtml") + cy.get(".opblock-get").first().click() + + cy.location().should((loc) => { + // The URL hash must change to reflect the deep link... + expect(loc.hash).to.not.equal("") + // ...but the pathname (and search) must NOT have been replaced by the + // browser resolving "#..." against the document . + expect(loc.pathname).to.equal("/") + expect(loc.search).to.contain("deepLinking=true") + expect(loc.search).to.contain("url=/documents/features/deep-linking.openapi.yaml") + }) + }) +}) diff --git a/test/unit/core/plugins/deep-linking/helpers.js b/test/unit/core/plugins/deep-linking/helpers.js new file mode 100644 index 00000000000..ea866b66612 --- /dev/null +++ b/test/unit/core/plugins/deep-linking/helpers.js @@ -0,0 +1,63 @@ +/** + * @prettier + */ +import { setHash } from "core/plugins/deep-linking/helpers" + +describe("deep-linking plugin - helpers", () => { + describe("setHash", () => { + let originalLocation + let pushStateSpy + + beforeEach(() => { + originalLocation = window.location + pushStateSpy = jest + .spyOn(window.history, "pushState") + .mockImplementation(() => {}) + }) + + afterEach(() => { + pushStateSpy.mockRestore() + Object.defineProperty(window, "location", { + configurable: true, + value: originalLocation, + }) + }) + + const stubLocation = (pathname, search) => { + Object.defineProperty(window, "location", { + configurable: true, + value: { ...originalLocation, pathname, search, hash: "" }, + }) + } + + it("should preserve the current pathname and search when pushing a hash", () => { + stubLocation("/app/api-docs", "?env=staging") + + setHash("/myTag/myOperation") + + expect(pushStateSpy).toHaveBeenCalledWith( + null, + null, + "/app/api-docs?env=staging#/myTag/myOperation" + ) + }) + + it("should still push a hash when the current URL has no search string", () => { + stubLocation("/swagger", "") + + setHash("/foo") + + expect(pushStateSpy).toHaveBeenCalledWith(null, null, "/swagger#/foo") + }) + + it("should clear window.location.hash when called with an empty value", () => { + stubLocation("/swagger", "") + window.location.hash = "#/foo" + + setHash("") + + expect(window.location.hash).toBe("") + expect(pushStateSpy).not.toHaveBeenCalled() + }) + }) +})