diff --git a/src/core/plugins/deep-linking/layout.js b/src/core/plugins/deep-linking/layout.js
index 7104240abf7e3aa1804a0eedf8ac6448568a83ef..0eefa8e701dd4f8adfd22a00ac62a8dd6dc675d2 100644
--- a/src/core/plugins/deep-linking/layout.js
+++ b/src/core/plugins/deep-linking/layout.js
@@ -73,18 +73,35 @@ export const parseDeepLinkHash = (rawHash) => ({ layoutActions, layoutSelectors,
hash = hash.slice(1)
}
- const hashArray = hash.split("/").map(val => (val || "").replace(/_/g, " "))
+ const hashArray = hash.split("/").map(val => (val || "").replace(/%20/g, " "))
const isShownKey = layoutSelectors.isShownKeyFromUrlHashArray(hashArray)
- const [type, tagId] = isShownKey
+ const [type, tagId, maybeOperationId] = isShownKey
if(type === "operations") {
// we're going to show an operation, so we need to expand the tag as well
- layoutActions.show(layoutSelectors.isShownKeyFromUrlHashArray([tagId]))
+ const tagIsShownKey = layoutSelectors.isShownKeyFromUrlHashArray([tagId])
+ layoutActions.show(tagIsShownKey)
+
+ // If an `_` is present, trigger the legacy escaping behavior to be safe
+ // TODO: remove this in v4.0, it is deprecated
+ if(tagId.indexOf("_") > -1) {
+ console.warn("Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead.")
+ layoutActions.show(tagIsShownKey.map(val => val.replace(/_/g, " ")), true)
+ }
+ }
+
+ layoutActions.show(isShownKey, true)
+
+ // If an `_` is present, trigger the legacy escaping behavior to be safe
+ // TODO: remove this in v4.0, it is deprecated
+ if (tagId.indexOf("_") > -1 || maybeOperationId.indexOf("_") > -1) {
+ console.warn("Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead.")
+ layoutActions.show(isShownKey.map(val => val.replace(/_/g, " ")), true)
}
- layoutActions.show(isShownKey, true) // TODO: 'show' operation tag
+ // Scroll to the newly expanded entity
layoutActions.scrollTo(isShownKey)
}
}
diff --git a/src/core/utils.js b/src/core/utils.js
index 3323a87dda5c680d7a269b225d1baf880a505ec3..1cee42b00019db9f5f61d6d0316d29c2a38ab7af 100644
--- a/src/core/utils.js
+++ b/src/core/utils.js
@@ -733,8 +733,10 @@ export function getAcceptControllingResponse(responses) {
return suitable2xxResponse || suitableDefaultResponse
}
-export const createDeepLinkPath = (str) => typeof str == "string" || str instanceof String ? str.trim().replace(/\s/g, "_") : ""
-export const escapeDeepLinkPath = (str) => cssEscape( createDeepLinkPath(str) )
+// suitable for use in URL fragments
+export const createDeepLinkPath = (str) => typeof str == "string" || str instanceof String ? str.trim().replace(/\s/g, "%20") : ""
+// suitable for use in CSS classes and ids
+export const escapeDeepLinkPath = (str) => cssEscape( createDeepLinkPath(str).replace(/%20/g, "_") )
export const getExtensions = (defObj) => defObj.filter((v, k) => /^x-/.test(k))
export const getCommonExtensions = (defObj) => defObj.filter((v, k) => /^pattern|maxLength|minLength|maximum|minimum/.test(k))
diff --git a/test/core/utils.js b/test/core/utils.js
index 8383c0fe2358e0b2751d04ce60e07038c2ed0baa..ce52f6dd68d53f75f403748fc41980c7db66f83e 100644
--- a/test/core/utils.js
+++ b/test/core/utils.js
@@ -991,12 +991,12 @@ describe("utils", function() {
describe("createDeepLinkPath", function() {
it("creates a deep link path replacing spaces with underscores", function() {
const result = createDeepLinkPath("tag id with spaces")
- expect(result).toEqual("tag_id_with_spaces")
+ expect(result).toEqual("tag%20id%20with%20spaces")
})
it("trims input when creating a deep link path", function() {
let result = createDeepLinkPath(" spaces before and after ")
- expect(result).toEqual("spaces_before_and_after")
+ expect(result).toEqual("spaces%20before%20and%20after")
result = createDeepLinkPath(" ")
expect(result).toEqual("")
@@ -1036,6 +1036,16 @@ describe("utils", function() {
const result = escapeDeepLinkPath("hello#world")
expect(result).toEqual("hello\\#world")
})
+
+ it("escapes a deep link path with a space", function() {
+ const result = escapeDeepLinkPath("hello world")
+ expect(result).toEqual("hello_world")
+ })
+
+ it("escapes a deep link path with a percent-encoded space", function() {
+ const result = escapeDeepLinkPath("hello%20world")
+ expect(result).toEqual("hello_world")
+ })
})
describe("getExtensions", function() {
diff --git a/test/e2e-cypress/static/documents/features/deep-linking.openapi.yaml b/test/e2e-cypress/static/documents/features/deep-linking.openapi.yaml
index b423ece3f690e1c3a9c78d79a7449f31faafd185..036f818c6646823700321cac248a522515ea33f5 100644
--- a/test/e2e-cypress/static/documents/features/deep-linking.openapi.yaml
+++ b/test/e2e-cypress/static/documents/features/deep-linking.openapi.yaml
@@ -18,6 +18,29 @@ paths:
operationId: "my Operation"
tags: ["my Tag"]
summary: an operation
+ responses:
+ '200':
+ description: a pet to be returned
+ content:
+ application/json:
+ schema:
+ type: object
+ /withUnderscores:
+ patch:
+ operationId: "underscore_Operation"
+ tags: ["underscore_Tag"]
+ summary: an operation
+ responses:
+ '200':
+ description: a pet to be returned
+ content:
+ application/json:
+ schema:
+ type: object
+ /noOperationId:
+ put:
+ tags: ["tagTwo"]
+ summary: some operations are anonymous...
responses:
'200':
description: a pet to be returned
diff --git a/test/e2e-cypress/static/documents/features/deep-linking.swagger.yaml b/test/e2e-cypress/static/documents/features/deep-linking.swagger.yaml
index 3eb90bf024af093dc95f1865c4c28c5f9568796d..60a085c71af29142c7fbc3e8f9c31d3006964579 100644
--- a/test/e2e-cypress/static/documents/features/deep-linking.swagger.yaml
+++ b/test/e2e-cypress/static/documents/features/deep-linking.swagger.yaml
@@ -17,3 +17,18 @@ paths:
responses:
"200":
description: ok
+ /withUnderscores:
+ patch:
+ operationId: "underscore_Operation"
+ tags: ["underscore_Tag"]
+ summary: an operation
+ responses:
+ "200":
+ description: ok
+ /noOperationId:
+ put:
+ tags: ["tagTwo"]
+ summary: an operation
+ responses:
+ "200":
+ description: ok
diff --git a/test/e2e-cypress/tests/deep-linking.js b/test/e2e-cypress/tests/deep-linking.js
index 39bdaf46949a8715b9fa6c699c2c81b25b3425f0..c2873fb6031d8aa26bf29eba44f20b1737f8a023 100644
--- a/test/e2e-cypress/tests/deep-linking.js
+++ b/test/e2e-cypress/tests/deep-linking.js
@@ -41,7 +41,83 @@ describe("Deep linking feature", () => {
describe("Operation with whitespace in tag+id", () => {
const elementToGet = ".opblock-post"
const correctElementId = "operations-my_Tag-my_Operation"
- const correctFragment = "#/my_Tag/my_Operation"
+ const correctFragment = "#/my%20Tag/my%20Operation"
+ const legacyFragment = "#/my_Tag/my_Operation"
+
+ it("should generate a correct element ID", () => {
+ cy.get(elementToGet)
+ .should("have.id", correctElementId)
+ })
+
+ it("should add the correct element fragment to the URL when expanded", () => {
+ cy.get(elementToGet)
+ .click()
+ .window()
+ .should("have.deep.property", "location.hash", correctFragment)
+ })
+
+ it("should provide an anchor link that has the correct fragment as href", () => {
+ cy.get(elementToGet)
+ .find("a")
+ .should("have.attr", "href", correctFragment)
+ .click()
+ .should("have.attr", "href", correctFragment) // should be valid after expanding
+
+ })
+
+ it("should expand the operation when reloaded", () => {
+ cy.visit(`${baseUrl}${correctFragment}`)
+ .reload()
+ .get(`${elementToGet}.is-open`)
+ .should("exist")
+ })
+
+ it("should expand the operation when reloaded and provided the legacy fragment", () => {
+ cy.visit(`${baseUrl}${legacyFragment}`)
+ .reload()
+ .get(`${elementToGet}.is-open`)
+ .should("exist")
+ })
+ })
+
+ describe("Operation with underscores in tag+id", () => {
+ const elementToGet = ".opblock-patch"
+ const correctElementId = "operations-underscore_Tag-underscore_Operation"
+ const correctFragment = "#/underscore_Tag/underscore_Operation"
+
+ it("should generate a correct element ID", () => {
+ cy.get(elementToGet)
+ .should("have.id", correctElementId)
+ })
+
+ it("should add the correct element fragment to the URL when expanded", () => {
+ cy.get(elementToGet)
+ .click()
+ .window()
+ .should("have.deep.property", "location.hash", correctFragment)
+ })
+
+ it("should provide an anchor link that has the correct fragment as href", () => {
+ cy.get(elementToGet)
+ .find("a")
+ .should("have.attr", "href", correctFragment)
+ .click()
+ .should("have.attr", "href", correctFragment) // should be valid after expanding
+
+ })
+
+ it("should expand the operation when reloaded", () => {
+ cy.visit(`${baseUrl}${correctFragment}`)
+ .reload()
+ .get(`${elementToGet}.is-open`)
+ .should("exist")
+ })
+ })
+
+ describe("Operation with no operationId", () => {
+ const elementToGet = ".opblock-put"
+ const correctElementId = "operations-tagTwo-put_noOperationId"
+ const correctFragment = "#/tagTwo/put_noOperationId"
it("should generate a correct element ID", () => {
cy.get(elementToGet)
@@ -127,7 +203,83 @@ describe("Deep linking feature", () => {
describe("Operation with whitespace in tag+id", () => {
const elementToGet = ".opblock-post"
const correctElementId = "operations-my_Tag-my_Operation"
- const correctFragment = "#/my_Tag/my_Operation"
+ const correctFragment = "#/my%20Tag/my%20Operation"
+ const legacyFragment = "#/my_Tag/my_Operation"
+
+ it("should generate a correct element ID", () => {
+ cy.get(elementToGet)
+ .should("have.id", correctElementId)
+ })
+
+ it("should add the correct element fragment to the URL when expanded", () => {
+ cy.get(elementToGet)
+ .click()
+ .window()
+ .should("have.deep.property", "location.hash", correctFragment)
+ })
+
+ it("should provide an anchor link that has the correct fragment as href", () => {
+ cy.get(elementToGet)
+ .find("a")
+ .should("have.attr", "href", correctFragment)
+ .click()
+ .should("have.attr", "href", correctFragment) // should be valid after expanding
+
+ })
+
+ it("should expand the operation when reloaded", () => {
+ cy.visit(`${baseUrl}${correctFragment}`)
+ .reload()
+ .get(`${elementToGet}.is-open`)
+ .should("exist")
+ })
+
+ it("should expand the operation when reloaded and provided the legacy fragment", () => {
+ cy.visit(`${baseUrl}${legacyFragment}`)
+ .reload()
+ .get(`${elementToGet}.is-open`)
+ .should("exist")
+ })
+ })
+
+ describe("Operation with underscores in tag+id", () => {
+ const elementToGet = ".opblock-patch"
+ const correctElementId = "operations-underscore_Tag-underscore_Operation"
+ const correctFragment = "#/underscore_Tag/underscore_Operation"
+
+ it("should generate a correct element ID", () => {
+ cy.get(elementToGet)
+ .should("have.id", correctElementId)
+ })
+
+ it("should add the correct element fragment to the URL when expanded", () => {
+ cy.get(elementToGet)
+ .click()
+ .window()
+ .should("have.deep.property", "location.hash", correctFragment)
+ })
+
+ it("should provide an anchor link that has the correct fragment as href", () => {
+ cy.get(elementToGet)
+ .find("a")
+ .should("have.attr", "href", correctFragment)
+ .click()
+ .should("have.attr", "href", correctFragment) // should be valid after expanding
+
+ })
+
+ it("should expand the operation when reloaded", () => {
+ cy.visit(`${baseUrl}${correctFragment}`)
+ .reload()
+ .get(`${elementToGet}.is-open`)
+ .should("exist")
+ })
+ })
+
+ describe("Operation with no operationId", () => {
+ const elementToGet = ".opblock-put"
+ const correctElementId = "operations-tagTwo-put_noOperationId"
+ const correctFragment = "#/tagTwo/put_noOperationId"
it("should generate a correct element ID", () => {
cy.get(elementToGet)