From fe0b63c299947470c9cdce3a0d00364a1e224bdb Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Thu, 22 Apr 2021 01:48:48 +1000 Subject: [PATCH] fix: improve handling of userland injected styles in component testing (#16024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(npm/react): do not clear head between tests * add a shared mount utils library * add readme * update dependencies * add mount utils to circle * change module Co-authored-by: Barthélémy Ledoux --- circle.yml | 13 +++ npm/mount-utils/CHANGELOG.md | 0 npm/mount-utils/README.md | 10 ++ npm/mount-utils/package.json | 28 ++++++ .../src/utils.ts => mount-utils/src/index.ts} | 82 ++++++++++++++++- npm/mount-utils/tsconfig.json | 52 +++++++++++ .../styled-components/issue-15879.spec.js | 79 ++++++++++++++++ npm/react/package.json | 1 + npm/react/rollup.config.js | 1 + npm/react/src/hooks.ts | 38 -------- npm/react/src/mount.ts | 61 ++---------- .../cypress/component/tailwind/redbox-spec.js | 20 +++- npm/vue/package.json | 1 + npm/vue/rollup.config.js | 1 + npm/vue/src/index.ts | 92 ++++++------------- yarn.lock | 21 +---- 16 files changed, 321 insertions(+), 179 deletions(-) create mode 100644 npm/mount-utils/CHANGELOG.md create mode 100644 npm/mount-utils/README.md create mode 100644 npm/mount-utils/package.json rename npm/{react/src/utils.ts => mount-utils/src/index.ts} (60%) create mode 100644 npm/mount-utils/tsconfig.json create mode 100644 npm/react/cypress/component/basic/styled-components/issue-15879.spec.js delete mode 100644 npm/react/src/hooks.ts diff --git a/circle.yml b/circle.yml index 8f92ab5e21..669e5160af 100644 --- a/circle.yml +++ b/circle.yml @@ -1237,6 +1237,15 @@ jobs: path: npm/react/test_results - store-npm-logs + npm-mount-utils: + <<: *defaults + steps: + - attach_workspace: + at: ~/ + - run: + name: Build + command: yarn workspace @cypress/mount-utils build + - store-npm-logs npm-create-cypress-tests: <<: *defaults @@ -1865,6 +1874,9 @@ linux-workflow: &linux-workflow - npm-react: requires: - build + - npm-mount-utils: + requires: + - build - npm-create-cypress-tests: requires: - build @@ -1880,6 +1892,7 @@ linux-workflow: &linux-workflow - npm-eslint-plugin-dev - npm-create-cypress-tests - npm-react + - npm-mount-utils - npm-vue - npm-design-system - npm-webpack-batteries-included-preprocessor diff --git a/npm/mount-utils/CHANGELOG.md b/npm/mount-utils/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/npm/mount-utils/README.md b/npm/mount-utils/README.md new file mode 100644 index 0000000000..9b0dd5bb27 --- /dev/null +++ b/npm/mount-utils/README.md @@ -0,0 +1,10 @@ +# @cypress/mount-utils + +> **Note** this package is not meant to be used outside of cypress component testing. + +This librares exports some shared types and utility functions designed to build adapters for components frameworks. + +It is used in: + +- [`@cypress/react`](https://github.com/cypress-io/cypress/tree/develop/npm/react) +- [`@cypress/vue`](https://github.com/cypress-io/cypress/tree/develop/npm/vue) diff --git a/npm/mount-utils/package.json b/npm/mount-utils/package.json new file mode 100644 index 0000000000..271d0b2571 --- /dev/null +++ b/npm/mount-utils/package.json @@ -0,0 +1,28 @@ +{ + "name": "@cypress/mount-utils", + "version": "0.0.0-development", + "description": "Shared utilities for the various component testing adapters", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "build-prod": "tsc", + "watch": "tsc -w" + }, + "dependencies": {}, + "devDependencies": { + "typescript": "^4.2.3" + }, + "files": [ + "dist" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cypress-io/cypress.git" + }, + "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/mount-utils#readme", + "bugs": "https://github.com/cypress-io/cypress/issues/new?template=1-bug-report.md", + "publishConfig": { + "access": "public" + } +} diff --git a/npm/react/src/utils.ts b/npm/mount-utils/src/index.ts similarity index 60% rename from npm/react/src/utils.ts rename to npm/mount-utils/src/index.ts index b39a816ac7..7ce7c45043 100644 --- a/npm/react/src/utils.ts +++ b/npm/mount-utils/src/index.ts @@ -1,4 +1,66 @@ -import { StyleOptions } from './mount' +/** + * Additional styles to inject into the document. + * A component might need 3rd party libraries from CDN, + * local CSS files and custom styles. + */ +export interface StyleOptions { + /** + * Creates element for each stylesheet + * @alias stylesheet + */ + stylesheets: string | string[] + /** + * Creates element for each stylesheet + * @alias stylesheets + */ + stylesheet: string | string[] + /** + * Creates element and inserts given CSS. + * @alias styles + */ + style: string | string[] + /** + * Creates element for each given CSS text. + * @alias style + */ + styles: string | string[] + /** + * Loads each file and creates a element + * with the loaded CSS + * @alias cssFile + */ + cssFiles: string | string[] + /** + * Single CSS file to load into a element + * @alias cssFile + */ + cssFile: string | string[] +} + +export const ROOT_ID = '__cy_root' + +/** + * Remove any style or extra link elements from the iframe placeholder + * left from any previous test + * + */ +export function cleanupStyles () { + const styles = document.body.querySelectorAll('[data-cy=injected-style-tag]') + + styles.forEach((styleElement) => { + if (styleElement.parentElement) { + styleElement.parentElement.removeChild(styleElement) + } + }) + + const links = document.body.querySelectorAll('[data-cy=injected-stylesheet]') + + links.forEach((link) => { + if (link.parentElement) { + link.parentElement.removeChild(link) + } + }) +} /** * Insert links to external style resources. @@ -14,6 +76,7 @@ function insertStylesheets ( link.type = 'text/css' link.rel = 'stylesheet' link.href = href + link.dataset.cy = 'injected-stylesheet' document.body.insertBefore(link, el) }) } @@ -25,6 +88,7 @@ function insertStyles (styles: string[], document: Document, el: HTMLElement | n styles.forEach((style) => { const styleElement = document.createElement('style') + styleElement.dataset.cy = 'injected-style-tag' styleElement.appendChild(document.createTextNode(style)) document.body.insertBefore(styleElement, el) }) @@ -124,3 +188,19 @@ export const injectStylesBeforeElement = ( return insertLocalCssFiles(cssFiles, document, el, options.log) } + +export function setupHooks (optionalCallback?: Function) { + // When running component specs, we cannot allow "cy.visit" + // because it will wipe out our preparation work, and does not make much sense + // thus we overwrite "cy.visit" to throw an error + Cypress.Commands.overwrite('visit', () => { + throw new Error( + 'cy.visit from a component spec is not allowed', + ) + }) + + beforeEach(() => { + optionalCallback?.() + cleanupStyles() + }) +} diff --git a/npm/mount-utils/tsconfig.json b/npm/mount-utils/tsconfig.json new file mode 100644 index 0000000000..44ef6c182e --- /dev/null +++ b/npm/mount-utils/tsconfig.json @@ -0,0 +1,52 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, + "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "skipLibCheck": true, + "lib": [ + "es2015", + "dom" + ] /* Specify library files to be included in the compilation: */, + "allowJs": true /* Allow javascript files to be compiled. */, + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true /* Generates corresponding '.d.ts' file. */, + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "dist" /* Redirect output structure to the directory. */, + // "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": false /* Enable all strict type-checking options. */, + // "noImplicitAny": true, + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + "types": ["cypress"] /* Type declaration files to be included in compilation. */, + "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "esModuleInterop": true + }, + "include": ["src"], + "exclude": ["node_modules", "*.js"] +} diff --git a/npm/react/cypress/component/basic/styled-components/issue-15879.spec.js b/npm/react/cypress/component/basic/styled-components/issue-15879.spec.js new file mode 100644 index 0000000000..ff80e2aaf6 --- /dev/null +++ b/npm/react/cypress/component/basic/styled-components/issue-15879.spec.js @@ -0,0 +1,79 @@ +import * as React from 'react' +import { mount } from '@cypress/react' +import styled, { ThemeProvider } from 'styled-components' + +const lightest = '#FFFEFD' +const light = '#FEFCF1' +const darker = '#C49A03' +const darkest = '#382E0A' + +export const theme = { + primaryDark: darkest, + primaryLight: lightest, + primaryLightDarker: light, + primaryHover: darker, +} + +const styledComponentsStyle = 'margin-bottom:1rem' +const Line = styled.div` + ${styledComponentsStyle} +` + +export const SearchResults = (props) => { + return ( +
+ {props.results.map((result) => { + return ( + + {result.title} + + ) + })} +
+ ) +} + +const mountComponent = ({ results }, options) => { + return mount( + +
+ +
+
, + options, + ) +} + +const inlineStyle = 'body { background: blue; }' +const bulmaCDN = 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.css' + +describe('SearchResults', () => { + it('should inject styles into ', () => { + mountComponent({ + results: [{ title: 'Org 1' }, { title: 'Org 2' }], + }, + { + stylesheets: [bulmaCDN], + style: inlineStyle, + }) + + cy.get('link').should('exist') + cy.get('link').should('have.attr', 'href', bulmaCDN) + }) + + it('style-components injected styles from previous test should not be cleaned up \ + but styles and stylesheets in mount should be', () => { + // style-components injected style should NOT have bene cleaned up + cy.get('style').should('contain.text', styledComponentsStyle) + + // cleaned up inline element and inserts given CSS. - * @alias styles - */ - style: string | string[] - /** - * Creates element for each given CSS text. - * @alias style - */ - styles: string | string[] - /** - * Loads each file and creates a element - * with the loaded CSS - * @alias cssFile - */ - cssFiles: string | string[] - /** - * Single CSS file to load into a element - * @alias cssFile - */ - cssFile: string | string[] -} - export interface MountReactComponentOptions { alias: string ReactDom: typeof ReactDOM diff --git a/npm/vue/cypress/component/tailwind/redbox-spec.js b/npm/vue/cypress/component/tailwind/redbox-spec.js index d6310b7e76..2965b21dd5 100644 --- a/npm/vue/cypress/component/tailwind/redbox-spec.js +++ b/npm/vue/cypress/component/tailwind/redbox-spec.js @@ -1,6 +1,10 @@ import { mount, mountCallback } from '@cypress/vue' import RedBox from './RedBox.vue' +const tailwindCdnLink = 'https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css' + +const inlineStyle = 'body { background: blue; }' + describe('RedBox 1', () => { const template = '' const options = { @@ -11,14 +15,17 @@ describe('RedBox 1', () => { }, // you can inject additional styles to be downloaded // + style: inlineStyle, stylesheets: [ // you can use external links - 'https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css', + tailwindCdnLink, ], } it('displays red Hello RedBox', () => { mount({ template }, options) + // shoud have injected the inline styling. + cy.get('style').should('contain.text', inlineStyle) cy.contains('Hello RedBox') cy.get('[data-cy=box]') @@ -38,12 +45,19 @@ describe('RedBox 2', () => { }, stylesheets: [ // you can use external links - 'https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css', + tailwindCdnLink, ], } - beforeEach(mountCallback({ template }, options)) + beforeEach(() => { + // should clean up links inserted via mounting options before each test. + cy.get('link').should('not.exist') + mount({ template }, options) + }) + it('displays Goodbye RedBox', () => { + // cleaned up inline