diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index 4b6dff933dff89acc25116af1748127e9dfd0f2e..0fb5cafac45156180b86e011ff9a942b082e7107 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -1,7 +1,7 @@ import React, { PureComponent } from "react" import PropTypes from "prop-types" import { getList } from "core/utils" -import { getExtensions, sanitizeUrl, createDeepLinkPath } from "core/utils" +import { getExtensions, sanitizeUrl, escapeDeepLinkPath } from "core/utils" import { Iterable, List } from "immutable" import ImPropTypes from "react-immutable-proptypes" @@ -112,7 +112,7 @@ export default class Operation extends PureComponent { let onChangeKey = [ path, method ] // Used to add values to _this_ operation ( indexed by path and method ) return ( -
+
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)