diff --git a/src/core/plugins/deep-linking/layout.js b/src/core/plugins/deep-linking/layout.js index bbd526cb9ff..23d3d85a533 100644 --- a/src/core/plugins/deep-linking/layout.js +++ b/src/core/plugins/deep-linking/layout.js @@ -66,13 +66,17 @@ export const parseDeepLinkHash = (rawHash) => ({ layoutActions, layoutSelectors, hash = hash.slice(1) } - if(hash[0] === "/") { - // "/pet/addPet" => "pet/addPet" - // makes the split result cleaner - // also handles forgotten leading slash - hash = hash.slice(1) + if(hash[0] !== "/") { + // Hash is not a Swagger UI deep link (which are always rooted at "/"). + // Treat it as an external/native anchor (e.g. "#model-Category") and + // leave the browser to handle scrolling. See #10527. + return } + // "/pet/addPet" => "pet/addPet" + // makes the split result cleaner + hash = hash.slice(1) + const hashArray = hash.split("/").map(val => (val || "")) const isShownKey = layoutSelectors.isShownKeyFromUrlHashArray(hashArray) diff --git a/test/e2e-cypress/e2e/bugs/10527.cy.js b/test/e2e-cypress/e2e/bugs/10527.cy.js new file mode 100644 index 00000000000..92296eae183 --- /dev/null +++ b/test/e2e-cypress/e2e/bugs/10527.cy.js @@ -0,0 +1,22 @@ +describe("#10527: External links to swagger-ui anchors don't work", () => { + const baseUrl = "/?deepLinking=true&url=/documents/bugs/10527.yaml" + + it("should not rewrite a hash anchor that targets a Model panel id", () => { + cy.visit(`${baseUrl}#model-Category`) + // give the deep-linking parser a chance to run + .get("#model-Category") + .should("exist") + cy.location().should((loc) => { + expect(loc.hash).to.eq("#model-Category") + }) + }) + + it("should still expand a deep-link rooted at '/' for an operation", () => { + cy.visit(`${baseUrl}#/pets/listPets`) + .get("#operations-pets-listPets.is-open") + .should("exist") + cy.location().should((loc) => { + expect(loc.hash).to.eq("#/pets/listPets") + }) + }) +}) diff --git a/test/e2e-cypress/static/documents/bugs/10527.yaml b/test/e2e-cypress/static/documents/bugs/10527.yaml new file mode 100644 index 00000000000..5247bbd4923 --- /dev/null +++ b/test/e2e-cypress/static/documents/bugs/10527.yaml @@ -0,0 +1,33 @@ +openapi: "3.0.4" +info: + title: Bug 10527 - external anchors should not be rewritten as deep links + version: "1.0" +paths: + /pets: + get: + operationId: "listPets" + tags: ["pets"] + summary: list pets + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" +components: + schemas: + Pet: + type: object + properties: + id: + type: integer + name: + type: string + Category: + type: object + properties: + id: + type: integer + name: + type: string diff --git a/test/unit/core/plugins/deep-linking/layout.js b/test/unit/core/plugins/deep-linking/layout.js new file mode 100644 index 00000000000..155b407d4c8 --- /dev/null +++ b/test/unit/core/plugins/deep-linking/layout.js @@ -0,0 +1,95 @@ +/** + * @prettier + */ + +import { parseDeepLinkHash } from "core/plugins/deep-linking/layout" + +describe("deep-linking plugin - parseDeepLinkHash", () => { + const makeSystem = ({ deepLinking = true } = {}) => { + const show = jest.fn() + const scrollTo = jest.fn() + return { + system: { + getConfigs: () => ({ deepLinking }), + layoutActions: { show, scrollTo }, + layoutSelectors: { + isShownKeyFromUrlHashArray: (hashArray) => { + const [tag, operationId] = hashArray + if (operationId) { + return ["operations", tag, operationId] + } else if (tag) { + return ["operations-tag", tag] + } + return [] + }, + }, + }, + show, + scrollTo, + } + } + + it("does not parse the hash when deepLinking is disabled", () => { + const { system, show, scrollTo } = makeSystem({ deepLinking: false }) + parseDeepLinkHash("#/myTag/myOperation")(system) + expect(show).not.toHaveBeenCalled() + expect(scrollTo).not.toHaveBeenCalled() + }) + + it("does nothing when the hash is empty", () => { + const { system, show, scrollTo } = makeSystem() + parseDeepLinkHash("")(system) + expect(show).not.toHaveBeenCalled() + expect(scrollTo).not.toHaveBeenCalled() + }) + + it("expands a tag deep link rooted at '/'", () => { + const { system, show, scrollTo } = makeSystem() + parseDeepLinkHash("#/myTag")(system) + expect(show).toHaveBeenLastCalledWith(["operations-tag", "myTag"], true) + expect(scrollTo).toHaveBeenCalledWith(["operations-tag", "myTag"]) + }) + + it("expands an operation deep link rooted at '/'", () => { + const { system, show, scrollTo } = makeSystem() + parseDeepLinkHash("#/myTag/myOperation")(system) + expect(show).toHaveBeenCalledWith(["operations-tag", "myTag"], true) + expect(show).toHaveBeenLastCalledWith( + ["operations", "myTag", "myOperation"], + true + ) + expect(scrollTo).toHaveBeenCalledWith([ + "operations", + "myTag", + "myOperation", + ]) + }) + + it("expands a deep link with the legacy '!/' shebang prefix", () => { + const { system, show, scrollTo } = makeSystem() + parseDeepLinkHash("#!/myTag/myOperation")(system) + expect(show).toHaveBeenLastCalledWith( + ["operations", "myTag", "myOperation"], + true + ) + expect(scrollTo).toHaveBeenCalledWith([ + "operations", + "myTag", + "myOperation", + ]) + }) + + it("ignores a native anchor hash that is not rooted at '/' (regression for #10527)", () => { + const { system, show, scrollTo } = makeSystem() + parseDeepLinkHash("#model-Category")(system) + expect(show).not.toHaveBeenCalled() + expect(scrollTo).not.toHaveBeenCalled() + }) + + it("ignores a single-token hash that is not rooted at '/' (regression for #10527)", () => { + const { system, show, scrollTo } = makeSystem() + parseDeepLinkHash("#some-section")(system) + expect(show).not.toHaveBeenCalled() + expect(scrollTo).not.toHaveBeenCalled() + }) +})