diff --git a/package-lock.json b/package-lock.json index c06df90bc6..2fad4344c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4634,6 +4634,10 @@ "resolved": "packages/node-fetch", "link": true }, + "node_modules/@k8slens/react-testing-library-discovery": { + "resolved": "packages/utility-features/react-testing-library-discovery", + "link": true + }, "node_modules/@k8slens/release-tool": { "resolved": "packages/release-tool", "link": true @@ -39139,6 +39143,129 @@ "lodash": "^4.17.21" } }, + "packages/utility-features/react-testing-library-discovery": { + "version": "1.0.0-alpha.0", + "license": "MIT", + "dependencies": { + "@testing-library/dom": "^8.19.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0" + } + }, + "packages/utility-features/react-testing-library-discovery/node_modules/@testing-library/dom": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz", + "integrity": "sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "packages/utility-features/react-testing-library-discovery/node_modules/@testing-library/react": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", + "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.5.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "packages/utility-features/react-testing-library-discovery/node_modules/@types/aria-query": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==" + }, + "packages/utility-features/react-testing-library-discovery/node_modules/@types/react-dom": { + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", + "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", + "dependencies": { + "@types/react": "*" + } + }, + "packages/utility-features/react-testing-library-discovery/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "packages/utility-features/react-testing-library-discovery/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "packages/utility-features/react-testing-library-discovery/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "packages/utility-features/react-testing-library-discovery/node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "packages/utility-features/react-testing-library-discovery/node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "packages/utility-features/react-testing-library-discovery/node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "packages/utility-features/run-many": { "name": "@k8slens/run-many", "version": "1.0.0-alpha.1", diff --git a/packages/utility-features/react-testing-library-discovery/.eslintrc.json b/packages/utility-features/react-testing-library-discovery/.eslintrc.json new file mode 100644 index 0000000000..b15115cb69 --- /dev/null +++ b/packages/utility-features/react-testing-library-discovery/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "@k8slens/eslint-config/eslint", + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/utility-features/react-testing-library-discovery/.prettierrc b/packages/utility-features/react-testing-library-discovery/.prettierrc new file mode 100644 index 0000000000..edd47b479e --- /dev/null +++ b/packages/utility-features/react-testing-library-discovery/.prettierrc @@ -0,0 +1 @@ +"@k8slens/eslint-config/prettier" diff --git a/packages/utility-features/react-testing-library-discovery/index.ts b/packages/utility-features/react-testing-library-discovery/index.ts new file mode 100644 index 0000000000..109c6af90f --- /dev/null +++ b/packages/utility-features/react-testing-library-discovery/index.ts @@ -0,0 +1,13 @@ +export type { + Discover, + GetSingleElement, + QueryAllElements, + QuerySingleElement, +} from "./src/discovery-of-html-elements"; + +export { + discoverFor, + getSingleElement, + queryAllElements, + querySingleElement, +} from "./src/discovery-of-html-elements"; diff --git a/packages/utility-features/react-testing-library-discovery/package.json b/packages/utility-features/react-testing-library-discovery/package.json new file mode 100644 index 0000000000..489549a187 --- /dev/null +++ b/packages/utility-features/react-testing-library-discovery/package.json @@ -0,0 +1,33 @@ +{ + "name": "@k8slens/react-testing-library-discovery", + "private": false, + "version": "1.0.0-alpha.0", + "description": "A way to discover HTML-elements using react-testing-library", + "type": "commonjs", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/lensapp/lens.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "author": { + "name": "OpenLens Authors", + "email": "info@k8slens.dev" + }, + "license": "MIT", + "homepage": "https://github.com/lensapp/lens", + "scripts": { + "build": "webpack", + "dev": "webpack --mode=development --watch", + "lint": "lens-lint", + "lint:fix": "lens-lint --fix" + }, + "dependencies": { + "@testing-library/dom": "^8.19.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0" + } +} diff --git a/packages/utility-features/react-testing-library-discovery/src/discovery-of-html-elements.ts b/packages/utility-features/react-testing-library-discovery/src/discovery-of-html-elements.ts new file mode 100644 index 0000000000..b49b7fab0a --- /dev/null +++ b/packages/utility-features/react-testing-library-discovery/src/discovery-of-html-elements.ts @@ -0,0 +1,132 @@ +import type { RenderResult } from "@testing-library/react"; +import { prettyDOM as prettyDom } from "@testing-library/dom"; + +type DiscoverySourceTypes = RenderResult | Element; + +export type QuerySingleElement = ( + attributeName: string, + attributeValue?: string +) => { discovered: Element | null } & Discover; + +type Clickable = { click: () => void }; + +export type GetSingleElement = ( + attributeName: string, + attributeValue?: string +) => { discovered: Element } & Discover & Clickable; + +export type QueryAllElements = (attributeName: string) => { + discovered: Element[]; + attributeValues: (string | null)[]; +}; + +export interface Discover { + querySingleElement: QuerySingleElement; + queryAllElements: QueryAllElements; + getSingleElement: GetSingleElement; +} + +export const discoverFor = ( + getSource: () => DiscoverySourceTypes +): Discover => ({ + querySingleElement: querySingleElement(getSource), + queryAllElements: queryAllElements(getSource), + getSingleElement: getSingleElement(getSource), +}); + +export const querySingleElement = + (getSource: () => DiscoverySourceTypes): QuerySingleElement => + (attributeName, attributeValue) => { + const source = getSource(); + + const dataAttribute = `data-${attributeName}-test`; + + const selector = attributeValue + ? `[${dataAttribute}="${attributeValue}"]` + : `[${dataAttribute}]`; + + const discovered = getBaseElement(source).querySelector(selector); + + const nestedDiscover = discoverFor(() => { + if (!discovered) { + throw new Error( + "Tried to do nested discover using source that does not exist" + ); + } + + return discovered; + }); + + return { + discovered, + + ...nestedDiscover, + }; + }; + +export const queryAllElements = + (getSource: () => DiscoverySourceTypes): QueryAllElements => + (attributeName) => { + const source = getSource(); + + const dataAttribute = `data-${attributeName}-test`; + + const results = [ + ...getBaseElement(source).querySelectorAll(`[${dataAttribute}]`), + ]; + + return { + discovered: results, + + attributeValues: results.map((result) => + result.getAttribute(dataAttribute) + ), + }; + }; + +export const getSingleElement = + (getSource: () => DiscoverySourceTypes): GetSingleElement => + (attributeName, attributeValue) => { + const dataAttribute = `data-${attributeName}-test`; + + const { discovered, ...nestedDiscover } = querySingleElement(getSource)( + attributeName, + attributeValue + ); + + if (!discovered) { + const html = prettyDom(getBaseElement(getSource())); + + if (attributeValue) { + const validValues = + queryAllElements(getSource)(attributeName).attributeValues; + + throw new Error( + `Couldn't find HTML-element with attribute "${dataAttribute}" with value "${attributeValue}".\n\nPresent values are:\n\n"${validValues.join( + '",\n"' + )}"\n\nHTML is:\n\n${html}` + ); + } + + throw new Error( + `Couldn't find HTML-element with attribute "${dataAttribute}"\n\nHTML is:\n\n${html}` + ); + } + + const click = () => { + if ("click" in discovered && typeof discovered.click === "function") { + discovered.click(); + } else { + throw new Error( + `Tried to click something that was not clickable:\n\n${prettyDom( + discovered + )}` + ); + } + }; + + return { discovered, click, ...nestedDiscover }; + }; + +const getBaseElement = (source: DiscoverySourceTypes) => + "baseElement" in source ? source.baseElement : source; diff --git a/packages/utility-features/react-testing-library-discovery/tsconfig.json b/packages/utility-features/react-testing-library-discovery/tsconfig.json new file mode 100644 index 0000000000..a4f6fa613e --- /dev/null +++ b/packages/utility-features/react-testing-library-discovery/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@k8slens/typescript/config/base.json" +} diff --git a/packages/utility-features/react-testing-library-discovery/webpack.config.js b/packages/utility-features/react-testing-library-discovery/webpack.config.js new file mode 100644 index 0000000000..3183f30179 --- /dev/null +++ b/packages/utility-features/react-testing-library-discovery/webpack.config.js @@ -0,0 +1 @@ +module.exports = require("@k8slens/webpack").configForNode;