diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index c044540..7f8b816 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -11,14 +11,17 @@ jobs: runs-on: ubuntu-latest # Job steps steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install dependencies - # 👇 Install dependencies with the same package manager used in the project (replace it as needed), e.g. yarn, npm, pnpm - run: npm i + run: npm ci # 👇 Adds Chromatic as a step in the workflow - name: Publish to Chromatic - uses: chromaui/action@v1 + uses: chromaui/action@latest # Chromatic GitHub Action options with: # 👇 Chromatic projectToken, refer to the manage page to obtain it. - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} \ No newline at end of file + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + onlyChanged: true \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js index fb1e263..4be8116 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,4 +1,6 @@ import path from 'path'; +import { mergeConfig } from 'vite'; +import turbosnap from 'vite-plugin-turbosnap'; /** @type { import('@storybook/web-components-vite').StorybookConfig } */ const config = { stories: ['../src/github/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], @@ -18,10 +20,21 @@ const config = { docs: { autodocs: 'tag', }, - async viteFinal(config, options) { + async viteFinal(config, { configType }) { // Ensures that the cache directory is inside the project directory config.cacheDir = path.join(__dirname, '../node_modules/.vite'); - return config; + + return mergeConfig(config, { + plugins: + configType === "PRODUCTION" + ? [ + turbosnap({ + // This should be the base path of your storybook. In monorepos, you may only need process.cwd(). + rootDir: config.root ?? process.cwd(), + }), + ] + : [], + }); }, }; export default config; diff --git a/.storybook/preview.js b/.storybook/preview.js index 6f54cf9..c581070 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -2,7 +2,7 @@ import { setCustomElementsManifest } from '@storybook/web-components'; import customElements from '../custom-elements.json'; import { globalTypesPrimer, decoratorsPrimer } from './primer-preview'; import { viewports } from './viewports'; -import { stringify, parseify } from '../src/utils'; +import { stringinator, parseify } from '../src/utils'; import "./storybook.css"; setCustomElementsManifest(customElements); @@ -15,7 +15,7 @@ global.attrGen = (args) => Object.entries(args) .map(([key, value]) => `\n ${key}="${value}"`) .join(' '); -global.stringify = stringify; +global.stringinator = stringinator; global.parseify = parseify; /** @type { import('@storybook/web-components').Preview } */ diff --git a/package-lock.json b/package-lock.json index bb22839..0275b0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,21 @@ "license": "MIT", "devDependencies": { "@chromaui/addon-visual-tests": "^1.0.0", - "@custom-elements-manifest/analyzer": "^0.9.0", + "@custom-elements-manifest/analyzer": "^0.9.3", "@custom-elements-manifest/to-markdown": "^0.1.0", + "@guidepup/virtual-screen-reader": "^0.21.0", "@primer/octicons": "^19.8.0", - "@primer/primitives": "^7.15.6", + "@primer/primitives": "^7.15.9", "commander": "^12.0.0", - "esbuild": "^0.20.0", + "esbuild": "^0.20.1", "esbuild-plugin-inline-import": "^1.0.4", "fs-extra": "^11.2.0", "jsdoc-to-markdown": "^8.0.1", + "storybook-addon-fetch-mock": "^1.0.1", "storydocker-storybook": "^0.0.22", "storydocker-utilities": "^0.0.16", - "yaml": "^2.3.4" + "vite-plugin-turbosnap": "^1.0.3", + "yaml": "^2.4.0" } }, "node_modules/@0no-co/graphql.web": { @@ -2122,9 +2125,9 @@ } }, "node_modules/@custom-elements-manifest/analyzer": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.9.0.tgz", - "integrity": "sha512-aYCc+CmUamFG3FbHO2m1hz/qik61pgYIJkN986k2BPamQmgomST1ZnRngDjUSt6RmO8NrZydxUzg+AKdEaE4EA==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.9.3.tgz", + "integrity": "sha512-UBTKxgoRy9n99saVxYSAy/NqOie1dDk9dsyGVPulW4vAxs7MeOHEbJ1vLvc4RhYs2dHm1VQH1xh41DirMxsJnQ==", "dev": true, "dependencies": { "@custom-elements-manifest/find-dependencies": "^0.0.5", @@ -2660,9 +2663,9 @@ "dev": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz", - "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", "cpu": [ "ppc64" ], @@ -2676,9 +2679,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz", - "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", "cpu": [ "arm" ], @@ -2692,9 +2695,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz", - "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", "cpu": [ "arm64" ], @@ -2708,9 +2711,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz", - "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", "cpu": [ "x64" ], @@ -2724,9 +2727,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz", - "integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", "cpu": [ "arm64" ], @@ -2740,9 +2743,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz", - "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", "cpu": [ "x64" ], @@ -2756,9 +2759,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz", - "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", "cpu": [ "arm64" ], @@ -2772,9 +2775,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz", - "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", "cpu": [ "x64" ], @@ -2788,9 +2791,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz", - "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", "cpu": [ "arm" ], @@ -2804,9 +2807,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz", - "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", "cpu": [ "arm64" ], @@ -2820,9 +2823,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz", - "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", "cpu": [ "ia32" ], @@ -2836,9 +2839,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz", - "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", "cpu": [ "loong64" ], @@ -2852,9 +2855,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz", - "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", "cpu": [ "mips64el" ], @@ -2868,9 +2871,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz", - "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", "cpu": [ "ppc64" ], @@ -2884,9 +2887,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz", - "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", "cpu": [ "riscv64" ], @@ -2900,9 +2903,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz", - "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", "cpu": [ "s390x" ], @@ -2916,9 +2919,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz", - "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", "cpu": [ "x64" ], @@ -2932,9 +2935,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz", - "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", "cpu": [ "x64" ], @@ -2948,9 +2951,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz", - "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", "cpu": [ "x64" ], @@ -2964,9 +2967,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz", - "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", "cpu": [ "x64" ], @@ -2980,9 +2983,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz", - "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", "cpu": [ "arm64" ], @@ -2996,9 +2999,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz", - "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", "cpu": [ "ia32" ], @@ -3012,9 +3015,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz", - "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", "cpu": [ "x64" ], @@ -3167,6 +3170,33 @@ "integrity": "sha512-u8A+DameixqpeyHzvnJWTGj+wfiskQOYHzSiJscCWVfMkIT3rxnbHMtGh3lMthaRY21nbUOK71WcsCnCrXhBJQ==", "dev": true }, + "node_modules/@guidepup/virtual-screen-reader": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@guidepup/virtual-screen-reader/-/virtual-screen-reader-0.21.0.tgz", + "integrity": "sha512-Zb7SokRuziuuLPLPQAJtwGBFqfr+u4R13fo1rS0KUEKQhYpKN9MuSpyssV3pLK3R4VXUCTq2pKjZ+Zd5d5pl9A==", + "dev": true, + "dependencies": { + "@testing-library/dom": "^9.3.3", + "@testing-library/user-event": "^14.5.2", + "aria-query": "^5.3.0", + "dom-accessibility-api": "^0.6.3" + } + }, + "node_modules/@guidepup/virtual-screen-reader/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@guidepup/virtual-screen-reader/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -4606,9 +4636,9 @@ } }, "node_modules/@primer/primitives": { - "version": "7.15.6", - "resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-7.15.6.tgz", - "integrity": "sha512-+wifGbIqc1m/vemDadp2gQfYgpO6u3AXmQJ/q9WomqmeHq40MhfyoDuplJJk4LBkXWeaD7V0fHlBl4Hu/DRRVA==", + "version": "7.15.9", + "resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-7.15.9.tgz", + "integrity": "sha512-1dDNxokYV8sP2QDHMaAaEnRTxhYNBznf4QDTe4Gx9VzOXLexRvqrhvRitDbefbyfvO0yPr5TKI2nGCoz5T5zrQ==", "dev": true }, "node_modules/@qiwi/multi-semantic-release": { @@ -12978,6 +13008,17 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-js": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz", + "integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", @@ -14346,9 +14387,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz", - "integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", "dev": true, "hasInstallScript": true, "bin": { @@ -14358,29 +14399,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.0", - "@esbuild/android-arm": "0.20.0", - "@esbuild/android-arm64": "0.20.0", - "@esbuild/android-x64": "0.20.0", - "@esbuild/darwin-arm64": "0.20.0", - "@esbuild/darwin-x64": "0.20.0", - "@esbuild/freebsd-arm64": "0.20.0", - "@esbuild/freebsd-x64": "0.20.0", - "@esbuild/linux-arm": "0.20.0", - "@esbuild/linux-arm64": "0.20.0", - "@esbuild/linux-ia32": "0.20.0", - "@esbuild/linux-loong64": "0.20.0", - "@esbuild/linux-mips64el": "0.20.0", - "@esbuild/linux-ppc64": "0.20.0", - "@esbuild/linux-riscv64": "0.20.0", - "@esbuild/linux-s390x": "0.20.0", - "@esbuild/linux-x64": "0.20.0", - "@esbuild/netbsd-x64": "0.20.0", - "@esbuild/openbsd-x64": "0.20.0", - "@esbuild/sunos-x64": "0.20.0", - "@esbuild/win32-arm64": "0.20.0", - "@esbuild/win32-ia32": "0.20.0", - "@esbuild/win32-x64": "0.20.0" + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" } }, "node_modules/esbuild-plugin-alias": { @@ -15254,6 +15295,71 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-mock": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", + "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.0.0", + "@babel/runtime": "^7.0.0", + "core-js": "^3.0.0", + "debug": "^4.1.1", + "glob-to-regexp": "^0.4.0", + "is-subset": "^0.1.1", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + }, + "engines": { + "node": ">=4.0.0" + }, + "funding": { + "type": "charity", + "url": "https://www.justgiving.com/refugee-support-europe" + }, + "peerDependencies": { + "node-fetch": "*" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/fetch-mock/node_modules/path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "dev": true + }, + "node_modules/fetch-mock/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/fetch-mock/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/fetch-mock/node_modules/whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "node_modules/fetch-retry": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", @@ -17510,6 +17616,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true + }, "node_modules/is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -20917,6 +21029,12 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -20959,6 +21077,12 @@ "integrity": "sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -27463,6 +27587,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -29822,6 +29956,23 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/storybook-addon-fetch-mock": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/storybook-addon-fetch-mock/-/storybook-addon-fetch-mock-1.0.1.tgz", + "integrity": "sha512-OrK9NzZkjhv5C+Nx7fgDIlg4UmDUp6W+HHSD0STJtxC9DxC2dumDY+tUAoc0DDgkchLK/qugKp5f07bfcY37aQ==", + "dev": true, + "dependencies": { + "fetch-mock": "^9.11.0" + }, + "peerDependencies": { + "@storybook/addons": "^6.5.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@storybook/addons": { + "optional": true + } + } + }, "node_modules/storybook-addon-mock": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/storybook-addon-mock/-/storybook-addon-mock-4.3.0.tgz", @@ -36634,6 +36785,12 @@ "node": ">=8" } }, + "node_modules/vite-plugin-turbosnap": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/vite-plugin-turbosnap/-/vite-plugin-turbosnap-1.0.3.tgz", + "integrity": "sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==", + "dev": true + }, "node_modules/vite-tsconfig-paths": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.1.tgz", @@ -38385,10 +38542,13 @@ "dev": true }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz", + "integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==", "dev": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } diff --git a/package.json b/package.json index 3b06516..25f3834 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "preview-storybook": "storybook dev", - "test-storybook": "test-storybook --coverage", + "test-storybook": "test-storybook --coverage --url http://localhost:6006", "predist": "npm run cem && npm run build-storybook", "dist": "node lib/esbuild.config.js", "prerelease": "npm run dist", @@ -45,18 +45,21 @@ "license": "MIT", "devDependencies": { "@chromaui/addon-visual-tests": "^1.0.0", - "@custom-elements-manifest/analyzer": "^0.9.0", + "@custom-elements-manifest/analyzer": "^0.9.3", "@custom-elements-manifest/to-markdown": "^0.1.0", + "@guidepup/virtual-screen-reader": "^0.21.0", "@primer/octicons": "^19.8.0", - "@primer/primitives": "^7.15.6", + "@primer/primitives": "^7.15.9", "commander": "^12.0.0", - "esbuild": "^0.20.0", + "esbuild": "^0.20.1", "esbuild-plugin-inline-import": "^1.0.4", "fs-extra": "^11.2.0", "jsdoc-to-markdown": "^8.0.1", + "storybook-addon-fetch-mock": "^1.0.1", "storydocker-storybook": "^0.0.22", "storydocker-utilities": "^0.0.16", - "yaml": "^2.3.4" + "vite-plugin-turbosnap": "^1.0.3", + "yaml": "^2.4.0" }, "customElements": "custom-elements.json" } diff --git a/src/devto/dsd.stories.js b/src/devto/dsd.stories.js index 03f6692..5055600 100644 --- a/src/devto/dsd.stories.js +++ b/src/devto/dsd.stories.js @@ -5,6 +5,12 @@ import { default as userScottnath } from './fixtures/generated/user--scottnath.j import { default as postProfileComponents } from './fixtures/generated/post--profile-components.json'; import { default as postDependabot } from './fixtures/generated/post--dependabot.json'; import { default as postBugfix } from './fixtures/generated/post--bugfix-multi-vite.json'; +import { getElements, ensureElements, ensureScreenRead } from './post/post.shared-spec'; +import { + getElements as getElementsUser, + ensureElements as ensureElementsUser, + ensureScreenRead as ensureScreenReadUser +} from './user/user.shared-spec'; import { post, dsd } from './index.js'; import docs from './dsd.docs.mdx'; @@ -48,6 +54,11 @@ export const Post = { args: { ...parseFetchedPost(postProfileComponents) }, + play: async ({ args, canvasElement, step }) => { + const elements = await getElements(canvasElement); + await ensureElements(elements, args); + await ensureScreenRead(elements, args); + } } export const User = { @@ -64,7 +75,21 @@ export const User = { }, args: { ...parseFetchedUser(userScottnath), - latest_post: stringify(parseFetchedPost(postDependabot)), - popular_post: stringify(parseFetchedPost(postBugfix)), + latest_post: stringinator(parseFetchedPost(postDependabot)), + popular_post: stringinator(parseFetchedPost(postBugfix)), }, + play: async ({ args, canvasElement, step }) => { + const elements = await getElementsUser(canvasElement); + const argsAfterFetch = { + ...args, + latest_post: { + ...parseFetchedPost(postDependabot), + }, + popular_post: { + ...parseFetchedPost(postBugfix), + }, + }; + await ensureElementsUser(elements, argsAfterFetch); + await ensureScreenReadUser(elements, argsAfterFetch); + } } diff --git a/src/devto/post/html.js b/src/devto/post/html.js index 4fbecd2..5ed0adc 100644 --- a/src/devto/post/html.js +++ b/src/devto/post/html.js @@ -17,9 +17,9 @@ function html(content) { return ` - `; diff --git a/src/devto/post/post.shared-spec.js b/src/devto/post/post.shared-spec.js index d4b73c9..748dacc 100644 --- a/src/devto/post/post.shared-spec.js +++ b/src/devto/post/post.shared-spec.js @@ -1,5 +1,9 @@ import { expect } from '@storybook/jest'; import { within as shadowWithin } from 'shadow-dom-testing-library'; +import { virtual } from '@guidepup/virtual-screen-reader'; + +import { spokenDLItem } from '../../utils/testing.js'; + /** * Extract elements from an shadow DOM element @@ -44,4 +48,44 @@ export const ensureElements = async (elements, args) => { await expect(elements.title).toHaveTextContent(args.title); await expect(elements.image).toBeTruthy(); await expect(elements.image).toHaveAttribute('src', args.cover_image); -} \ No newline at end of file +} + +/** + * Extract the expected screen reader spoken output + * @param {ForemPostHTML} args - a content object representing a DEV post + * @returns {string[]} - array of strings representing the expected screen reader output + */ +export const getExpectedScreenText = (args) => { + const expected = ['dev.to article']; + + // uses `spokenDLItem` to create dt/dd spoken pairs + const dlItem = new spokenDLItem(expected); + + if (args.error) { + expected.push(args.error); + } else { + expected.push(`link, article ${args.title}`); + expected.push(`img, Cover image for article ${args.title}`); + expected.push(`end of link, article ${args.title}`); + } + + return expected; +} + +/** + * Ensure the screen reader reads the correct content + */ +export const ensureScreenRead = async (elements, args) => { + const expected = getExpectedScreenText(args); + // Start virtual screen reader + await virtual.start({ container: elements.container }); + while ((await virtual.lastSpokenPhrase()) !== expected[expected.length - 1]) { + await virtual.next(); + } + + // Compare spoken phrases to expected + expect(await virtual.spokenPhraseLog()).toEqual(expected); + + // Stop virtual screen reader + await virtual.stop(); +} diff --git a/src/devto/post/post.stories.js b/src/devto/post/post.stories.js index f8115ea..012e126 100644 --- a/src/devto/post/post.stories.js +++ b/src/devto/post/post.stories.js @@ -3,8 +3,7 @@ import { generateMockResponse } from '../helpers/testing'; import { parseFetchedPost } from './content'; import { default as postDependabot } from '../fixtures/generated/post--dependabot.json'; import { default as postProfileComponents } from '../fixtures/generated/post--profile-components.json'; - -import { getElements, ensureElements } from './post.shared-spec'; +import { getElements, ensureElements, ensureScreenRead } from './post.shared-spec'; import './index.js'; @@ -18,7 +17,7 @@ export default { return ` `; - } + }, }; @@ -29,6 +28,7 @@ export const Post = { play: async ({ args, canvasElement, step }) => { const elements = await getElements(canvasElement); await ensureElements(elements, args); + await ensureScreenRead(elements, args); } } @@ -49,6 +49,7 @@ export const Fetch = { ...args, }; await ensureElements(elements, argsAfterFetch); + await ensureScreenRead(elements, argsAfterFetch); } } @@ -70,6 +71,7 @@ export const FetchOverides = { ...args, }; await ensureElements(elements, argsAfterFetch); + await ensureScreenRead(elements, argsAfterFetch); } } @@ -90,5 +92,6 @@ export const FetchError = { error: `Fetch Error: Post "${args.id}" not found`, }; await ensureElements(elements, argsAfterFetch); + await ensureScreenRead(elements, argsAfterFetch); } } diff --git a/src/devto/styles/styles.css b/src/devto/styles/styles.css index 6f3aa1c..64a8218 100644 --- a/src/devto/styles/styles.css +++ b/src/devto/styles/styles.css @@ -39,14 +39,14 @@ section[itemscope] { box-shadow: 0 0 0 1px var(--color-shadow); background: var(--card-bg); - & header { + header:first-child { &::before { content: " "; display: block; background-color: var(--color-profile); height: var(--heading-height); } - & span:has([itemprop="memberOf"]) { + span:has([itemprop="memberOf"]) { display: inline-block; background: var(--svg-dev-logo) no-repeat white; background-position: center -2px; @@ -61,7 +61,7 @@ section[itemscope] { right: var(--side-spacing); top: calc(calc(var(--heading-height) - var(--logo-size)) / 2); - & span { + span { position: absolute; top: calc(var(--logo-size) * -3); font-size: .1em; @@ -176,16 +176,19 @@ section[itemscope] { } } - & dl:has(.post) { + div:has(.post) { border-bottom: 1px solid var(--color-shadow); padding-bottom: 1em; - & dt { + header { color: var(--color-light); font-size: var(--font-size-light); font-weight: var(--font-weight-light); border-top: 1px solid var(--color-shadow); margin-top: .5em; + &::before { + display: none; + } } } } diff --git a/src/devto/user/content.js b/src/devto/user/content.js index 1bccfef..1fe1e32 100644 --- a/src/devto/user/content.js +++ b/src/devto/user/content.js @@ -30,6 +30,7 @@ const blankPng = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1 * @property {number} [post_count] - The number of posts the user has published * @property {ForemPostHTML} [latest_post] - User's latest post * @property {ForemPostHTML} [popular_post] - User's most popular post + * @property {Object} [a11y] - accessibility content * @memberof DEVUtils.user */ @@ -98,6 +99,7 @@ export const parseFetchedUser = (user = {}) => { post_count: user.post_count, latest_post: parsePostString(user.latest_post), popular_post: parsePostString(user.popular_post), + a11y: user.a11y || {}, } const usr = {}; // remove `undefined` values @@ -107,6 +109,18 @@ export const parseFetchedUser = (user = {}) => { return usr; } +export const a11yContent = (content) => { + let headerLabel = `dev.to user ${content.username}`; + if (content.name) { + headerLabel = headerLabel.replace(content.username, `${content.name}, username ${content.username}`); + } + content.a11y = { + ...content.a11y, + headerLabel, + } + return content; +} + /** * Parses and cleans user content to match what is expected by the user HTML * @param {ForemUserHTML} content - a content object representing a dev.to user @@ -129,7 +143,7 @@ export const cleanUserContent = (content = {}) => { } content.latest_post.cover_image = content.latest_post.cover_image || blankPng; } - return content; + return a11yContent(content); } /** diff --git a/src/devto/user/html.js b/src/devto/user/html.js index a98bdcc..f9bdb0d 100644 --- a/src/devto/user/html.js +++ b/src/devto/user/html.js @@ -19,14 +19,14 @@ function html(content) { return `
-
- dev.to user +
+
-
@@ -35,16 +35,16 @@ function html(content) { ${content.joined_at ? `

Joined on

` : ''} ${content.post_count ? `

${content.post_count} posts published

` : ''} ${content.latest_post || content.popular_post ? ` -
+
${content.latest_post ? ` -
Latest post
-
${postHTML(content.latest_post)}
+
Latest post
+ ${postHTML(content.latest_post)} ` : ''} ${content.popular_post ? ` -
Popular post
-
${postHTML(content.popular_post)}
+
Popular post
+ ${postHTML(content.popular_post)} ` : ''} -
+ ` : ''}
` } return ` -
- +
+ ${content.org ? ` - ${content.org} / + ` : ''} - ${content.name} + ${content.description ? `

${content.description}

@@ -46,7 +46,7 @@ function html(content) {
${content.forks_count}
` : ''} -
+ `; } diff --git a/src/github/repository/repository.shared-spec.js b/src/github/repository/repository.shared-spec.js index 87c09ee..f89b5de 100644 --- a/src/github/repository/repository.shared-spec.js +++ b/src/github/repository/repository.shared-spec.js @@ -1,5 +1,8 @@ import { expect } from '@storybook/jest'; import { within as shadowWithin } from 'shadow-dom-testing-library'; +import { virtual } from '@guidepup/virtual-screen-reader'; + +import { spokenDLItem } from '../../utils/testing.js'; /** * Extract elements from an shadow DOM element @@ -75,4 +78,61 @@ export const ensureElements = async (elements, args) => { await expect(elements.langDetails).toBeFalsy(); await expect(elements.langTerm).toBeFalsy(); } -} \ No newline at end of file +} + +/** + * Extract the expected screen reader spoken output + * @param {GitHubRepositoryHTML} args - a content object representing a GitHub repository + * @returns {string[]} - array of strings representing the expected screen reader output + */ +export const getExpectedScreenText = (args) => { + const expected = ['region, GitHub repository']; + + // uses `spokenDLItem` to create dt/dd spoken pairs + const dlItem = new spokenDLItem(expected); + + if (args.error) { + expected.push(args.error); + } else { + expected.push(`link, ${args.full_name} repository on GitHub`); + + if (args.description) { + expected.push(args.description) + } + // start of description list + expected.push('Repository details'); + if (args.language) { + dlItem.spoken('Language', args.language); + } + if (args.stargazers_count && args.stargazers_count > 0) { + dlItem.spoken('Stars', args.stargazers_count); + } + if (args.subscribers_count && args.subscribers_count > 0) { + dlItem.spoken('Watchers', args.subscribers_count); + } + if (args.forks_count && args.forks_count > 0) { + dlItem.spoken('Forks', args.forks_count); + } + } + + expected.push('end of region, GitHub repository'); + return expected; +} + +/** + * Ensure the screen reader reads the correct content + */ +export const ensureScreenRead = async (elements, args) => { + const expected = getExpectedScreenText(args); + // Start virtual screen reader + await virtual.start({ container: elements.container }); + while ((await virtual.lastSpokenPhrase()) !== expected[expected.length - 1]) { + await virtual.next(); + } + + // Compare spoken phrases to expected + expect(await virtual.spokenPhraseLog()).toEqual(expected); + + // Stop virtual screen reader + await virtual.stop(); +} diff --git a/src/github/repository/repository.stories.js b/src/github/repository/repository.stories.js index 059f555..bd5b8cb 100644 --- a/src/github/repository/repository.stories.js +++ b/src/github/repository/repository.stories.js @@ -1,7 +1,6 @@ - import { generateMockResponse } from '../helpers/testing'; import { parseFetchedRepo } from './content'; -import { getElements, ensureElements } from './repository.shared-spec'; +import { getElements, ensureElements, ensureScreenRead } from './repository.shared-spec'; import { repoProfileComponents, repoFreeCodeCamp } from '../fixtures'; import { primerThemes } from '../../../.storybook/primer-preview.js'; @@ -17,7 +16,7 @@ export default { return ` `; - } + }, }; export const Repository = { @@ -27,6 +26,7 @@ export const Repository = { play: async ({ args, canvasElement, step }) => { const elements = await getElements(canvasElement); await ensureElements(elements, args); + await ensureScreenRead(elements, args); } } @@ -69,10 +69,7 @@ export const Theme = { ...parseFetchedRepo(repoFreeCodeCamp), theme: 'dark', }, - play: async ({ args, canvasElement, step }) => { - const elements = await getElements(canvasElement); - await ensureElements(elements, args); - } + play: Repository.play, } export const Fetch = { @@ -92,6 +89,7 @@ export const Fetch = { ...args, }; await ensureElements(elements, argsAfterFetch); + await ensureScreenRead(elements, argsAfterFetch); } } @@ -121,11 +119,20 @@ export const FetchError = { error: `Fetch Error: Repo "${args.full_name}" not found`, }; await ensureElements(elements, argsAfterFetch); + await ensureScreenRead(elements, argsAfterFetch); } } export const NoRepo = { - play: Repository.play, + play: async ({ args, canvasElement, step }) => { + const elements = await getElements(canvasElement); + const argsAfterFetch = { + ...args, + error: 'Missing repo attribute: `full_name`', + }; + await ensureElements(elements, argsAfterFetch); + await ensureScreenRead(elements, argsAfterFetch); + } }; const themesRender = (args) => { diff --git a/src/github/styles/user.css b/src/github/styles/user.css index ce9166c..5a9c2f0 100644 --- a/src/github/styles/user.css +++ b/src/github/styles/user.css @@ -10,7 +10,7 @@ section[itemscope] { overflow-wrap: anywhere; overflow: hidden; } -:host header { +:host header:first-child { background-color: var(--bg-color-light); padding: var(--row-spacing); @@ -18,7 +18,7 @@ section[itemscope] { gap: var(--svg-gap); height: calc(var(--logo-size) + var(--logo-outline-offset) * 2); - > span:has([itemprop="memberOf"]) { + [itemprop="memberOf"] { background-color: var(--color-normal); width: var(--logo-size); height: var(--logo-size); @@ -144,7 +144,7 @@ section[itemscope] { mask-image: var(--svg-people); } - > span { + :is(div) { display: inline-flex; flex-direction: row-reverse; flex-wrap: nowrap; diff --git a/src/github/user/content.js b/src/github/user/content.js index d48a6e6..e47d0b1 100644 --- a/src/github/user/content.js +++ b/src/github/user/content.js @@ -25,6 +25,7 @@ const blankPng = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1 * @property {string} [following] - number of people user is following * @property {string} [followers] - number of followers * @property {string} [error] - error message, if any + * @property {Object} [a11y] - accessibility content * @property {Array} [repositories] - array of repositories */ @@ -62,6 +63,7 @@ export const parseFetchedUser = (user = {}) => { bio: user.bio, following: user.following, followers: user.followers, + a11y: user.a11y || {}, } } @@ -136,6 +138,18 @@ export const cleanUserContent = (content = {}) => { return c; }; +export const a11yContent = (content) => { + let headerLabel = `GitHub user ${content.login}`; + if (content.name) { + headerLabel = headerLabel.replace(content.login, `${content.name}, username ${content.login}`); + } + content.a11y = { + ...content.a11y, + headerLabel, + } + return content; +} + /** * Generates an object of content for the repository HTML * @param {GitHubUserHTML} content @@ -171,6 +185,6 @@ export const generateUserContent = async (content, fetch = false) => { } userFromContent.repositories = Array.from(repos); } - return Object.assign({}, fetched, userFromContent); + return a11yContent(Object.assign({}, fetched, userFromContent)); } \ No newline at end of file diff --git a/src/github/user/html.js b/src/github/user/html.js index c365264..e5d0b0d 100644 --- a/src/github/user/html.js +++ b/src/github/user/html.js @@ -21,17 +21,17 @@ function html(content) { return `
-
- GitHub user - ${content.login} +
+ +
-