diff --git a/snapshots/theme-switch-test-js-screenshot-tests-when-user-specifies-a-custom-color-for-switch-icon-in-a-css-rule-with-low-specificity-like-html-the-colors-should-be-applied-1-snap.png b/snapshots/theme-switch-test-js-screenshot-tests-when-user-specifies-a-custom-color-for-switch-icon-in-a-css-rule-with-low-specificity-like-html-the-colors-should-be-applied-1-snap.png new file mode 100644 index 0000000..e76ae63 Binary files /dev/null and b/snapshots/theme-switch-test-js-screenshot-tests-when-user-specifies-a-custom-color-for-switch-icon-in-a-css-rule-with-low-specificity-like-html-the-colors-should-be-applied-1-snap.png differ diff --git a/snapshots/theme-switch-test-js-screenshot-tests-when-user-specifies-a-custom-color-for-switch-icon-the-colors-should-be-applied-1-snap.png b/snapshots/theme-switch-test-js-screenshot-tests-when-user-specifies-a-custom-color-for-switch-icon-the-colors-should-be-applied-1-snap.png new file mode 100644 index 0000000..794a9ca Binary files /dev/null and b/snapshots/theme-switch-test-js-screenshot-tests-when-user-specifies-a-custom-color-for-switch-icon-the-colors-should-be-applied-1-snap.png differ diff --git a/test2.html b/test2.html new file mode 100644 index 0000000..0ed9178 --- /dev/null +++ b/test2.html @@ -0,0 +1,21 @@ + + + + + Theme switch test 2 + + + + + + + + + diff --git a/theme-switch.js b/theme-switch.js index 27ed4bf..1c5e8d2 100644 --- a/theme-switch.js +++ b/theme-switch.js @@ -200,12 +200,9 @@ function generateStyle() { cursor: pointer; } - /* Only change the color of the core and not rays */ - /* as it seems to make a visually better animation */ - #circle { - fill: var(--theme-switch-icon-color); - stroke: var(--theme-switch-icon-color); - } + #circle { fill: var(--theme-switch-icon-color, #000); } + + #rays { stroke: var(--theme-switch-icon-color, #000); } `; } diff --git a/theme-switch.min.js b/theme-switch.min.js index 41369e0..2d627e0 100644 --- a/theme-switch.min.js +++ b/theme-switch.min.js @@ -59,12 +59,9 @@ cursor: pointer; } - /* Only change the color of the core and not rays */ - /* as it seems to make a visually better animation */ - #circle { - fill: var(--theme-switch-icon-color); - stroke: var(--theme-switch-icon-color); - } + #circle { fill: var(--theme-switch-icon-color, #000); } + + #rays { stroke: var(--theme-switch-icon-color, #000); } `}p();window.customElements.define("theme-switch",m);window.matchMedia(h).addEventListener("change",p);function p(){let b=q();if(b===c)b=r();document.documentElement.setAttribute(g,b)}function q(){const c=localStorage.getItem(b);return c===null?f:c}function r(){const b=window.matchMedia(h).matches;return b?d:e}function s(){let b=q();if(b===c){return i}else if(b===d){return j}else{return k}}function t(){let f=q();if(f===c){localStorage.setItem(b,e);u()}else if(f===d){localStorage.setItem(b,c);v()}else{localStorage.setItem(b,d);w()}p()}function u(){l.getElementById("letter-anim-hide").beginElement();l.getElementById("core-anim-shrink").beginElement();l.getElementById("rays-anim-rotate").beginElement();l.getElementById("rays-anim-show").beginElement()}function v(){l.getElementById("eclipse-anim-go").beginElement();l.getElementById("letter-anim-show").beginElement()}function w(){l.getElementById("rays-anim-hide").beginElement();l.getElementById("core-anim-enlarge").beginElement();l.getElementById("eclipse-anim-come").beginElement()}})(); //# sourceMappingURL=theme-switch.min.js.map \ No newline at end of file diff --git a/theme-switch.min.js.map b/theme-switch.min.js.map index 48632c0..72afa58 100644 --- a/theme-switch.min.js.map +++ b/theme-switch.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["theme-switch.js"],"names":[],"mappings":"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","file":"theme-switch.min.js","sourcesContent":["/*\r\n* NOTE: Do not use this script as an ES6 module.\r\n* ES6 modules are deferred and we don't want that because\r\n* we want the user previous theme selection to be applied\r\n* as soon as possible (before the page is rendered).\r\n* */\r\n\r\n/*\r\n* There are two types of modules mostly used in JavaScript.\r\n* One is created by Node.js and is used inside the Node environment\r\n* and has been available for a long time. It is called CommonJS.\r\n* Another is the standard native JavaScript modules introduced in ES6.\r\n*\r\n* The Node variant (CommonJS) uses `module.exports` (or simply `exports`) and\r\n* `require()` to export and import scripts, functions, variables, etc.\r\n* Browsers do not know about `exports` or `require` functions and throw error\r\n* because they are objects and functions created just in Node environment and set globally.\r\n* If you want to use this type of module in browsers, you should bundle the files\r\n* (merge all of them into a single JS file which eliminates the need for exports and require)\r\n* with tools like babel, webpack, rollup, etc.\r\n*\r\n* ES6 modules use `export` and `import` keywords for the same purpose.\r\n*\r\n* Example Node modules:\r\n*\r\n* my-calculator.js\r\n* const PI = 3.14;\r\n* function calculate() {}\r\n* modules.exports.calculate = calculate;\r\n* modules.exports.PI = PI;\r\n*\r\n* main.js\r\n* const calculator = require(\"my-calculator\");\r\n* let perimeter = 2 * calculator.PI;\r\n* let result = calculator.calculate();\r\n*\r\n* Example ES6 modules:\r\n*\r\n* my-calculator.js\r\n* export const PI = 3.14;\r\n* export function calculate() {}\r\n*\r\n* main.js\r\n* import { calculate, PI } from \"my-calculator.js\";\r\n* let perimeter = 2 * PI;\r\n* let result = calculate();\r\n*\r\n* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules\r\n* and https://stackoverflow.com/a/9901097/8583692\r\n* */\r\n\r\n/*\r\n* Minify the script either through command line with babel:\r\n* - babel theme-switch.js --source-maps --out-file result.min.js\r\n* or with babel-minify (also has an alias called minify) which is useful if\r\n* you don't already use babel (as a preset) or want to run minification standalone.\r\n* Note that it does not take into account babel.config.json settings.\r\n* - babel-minify (or minify) theme-switch.js --mangle --no-comments --out-file result.min.js`\r\n* Or automate it with IntelliJ file watcher\r\n* - babel\r\n* + program: $ProjectFileDir$\\node_modules\\.bin\\babel\r\n* + arguments: $FilePathRelativeToProjectRoot$ --out-file $FileNameWithoutExtension$.min.js --source-maps\r\n* Note that setting \"sourceMaps\": \"true\" in babel.config.json does not work because of\r\n* this bug: https://github.com/babel/babel/issues/5261 (\"sourceMaps\": \"inline\" works, however)\r\n* - UglifyJS\r\n* - YUI compressor (seems to be deprecated and removed in newer versions of IntelliJ)\r\n*\r\n* Babel preset-env inserts a semicolon at the start of the minified file.\r\n* See why: https://stackoverflow.com/q/1873983/8583692\r\n* */\r\n\r\n// TODO: extract Jest configuration to a jest.config.js file\r\n\r\n/*\r\n* NOTE: To avoid name collisions if another script declares variables or functions with the same name\r\n* as ours (i.e. defining them in the global scope) and browsers complaining about identifiers\r\n* being redeclared, we wrap all our code in a closure or IIFE (sort of creating a namespace for it).\r\n* ES6 now supports block scope as well (simply wrapping the whole code in {}).\r\n* I am now using the babel-plugin-iife-wrap plugin to wrap the whole result (minified)\r\n* code in an IIFE.\r\n* For examples, see these libraries:\r\n* - https://github.com/highlightjs/highlight.js/blob/main/src/highlight.js\r\n* - https://github.com/jashkenas/underscore/blob/master/underscore.js\r\n* See\r\n* - https://www.w3schools.com/js/js_scope.asp\r\n* - https://github.com/jhnns/rewire/issues/136#issuecomment-380829197\r\n* - https://stackoverflow.com/a/32750216/8583692\r\n* - https://stackoverflow.com/q/8228281/8583692\r\n* - https://stackoverflow.com/q/881515/8583692\r\n* - https://stackoverflow.com/q/39388777/8583692\r\n* - https://stackoverflow.com/a/47207686/8583692\r\n* We could also do something like these libraries:\r\n* - https://github.com/juliangarnier/anime/blob/master/build.js\r\n* - https://github.com/mrdoob/three.js/\r\n* - https://github.com/moment/moment\r\n* - https://github.com/floating-ui/floating-ui\r\n* */\r\n\r\nconst ICON_SIZE = 24 /* px */;\r\nconst THEME_KEY = \"theme\";\r\nconst THEME_AUTO = \"auto\";\r\nconst THEME_DARK = \"dark\";\r\nconst THEME_LIGHT = \"light\";\r\nconst THEME_DEFAULT = THEME_LIGHT;\r\nconst THEME_ATTRIBUTE = \"data-theme\";\r\nconst COLOR_SCHEME_DARK = \"(prefers-color-scheme: dark)\";\r\n\r\n// circleRadius, raysOpacity, eclipseCenterX, letterOffset\r\nconst ICON_INITIAL_STATE_FOR_AUTO = [10, 0, 33, 0];\r\nconst ICON_INITIAL_STATE_FOR_DARK = [10, 0, 20, 1];\r\nconst ICON_INITIAL_STATE_FOR_LIGHT = [5, 1, 33, 1];\r\n\r\nlet shadowRoot;\r\n\r\nclass ThemeSwitchElement extends HTMLElement {\r\n constructor() {\r\n super();\r\n\r\n // See https://stackoverflow.com/q/2305654/8583692\r\n shadowRoot = this.attachShadow({mode: \"open\"});\r\n shadowRoot.innerHTML = generateIcon(...getInitialStateForIcon());\r\n\r\n // Add the click listener to the top-most parent (the custom element itself)\r\n // so the padding etc. on the element be also clickable\r\n shadowRoot.host.addEventListener(\"click\", toggleTheme);\r\n\r\n // Create some CSS to apply to the shadow DOM\r\n // See https://css-tricks.com/styling-a-web-component/\r\n const style = document.createElement(\"style\");\r\n style.textContent = generateStyle();\r\n shadowRoot.append(style);\r\n }\r\n}\r\n\r\n// language=html\r\nfunction generateIcon(circleRadius, raysOpacity, eclipseCenterX, letterOffset) {\r\n return `\r\n \r\n \r\n `;\r\n}\r\n\r\n// language=css\r\nfunction generateStyle() {\r\n return `\r\n /* :host === the host element of the shadow === */\r\n /* See https://developer.mozilla.org/en-US/docs/Web/CSS/:host */\r\n :host {\r\n display: flex;\r\n width: ${ICON_SIZE}px;\r\n aspect-ratio: 1 / 1;\r\n /* This is for when the element has padding */\r\n cursor: pointer;\r\n }\r\n\r\n button {\r\n padding: 0;\r\n border: none;\r\n background: transparent;\r\n display: flex;\r\n /* The host element also has its cursor set */\r\n cursor: pointer;\r\n }\r\n\r\n /* Only change the color of the core and not rays */\r\n /* as it seems to make a visually better animation */\r\n #circle {\r\n fill: var(--theme-switch-icon-color);\r\n stroke: var(--theme-switch-icon-color);\r\n }\r\n `;\r\n}\r\n\r\nupdateTheme();\r\nwindow.customElements.define(\"theme-switch\", ThemeSwitchElement);\r\nwindow\r\n .matchMedia(COLOR_SCHEME_DARK)\r\n .addEventListener(\"change\", updateTheme);\r\n\r\nfunction updateTheme() {\r\n let theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) theme = getSystemTheme();\r\n document.documentElement.setAttribute(THEME_ATTRIBUTE, theme);\r\n}\r\n\r\nfunction getUserThemeSelection() {\r\n const userSelection = localStorage.getItem(THEME_KEY);\r\n return userSelection === null ? THEME_DEFAULT : userSelection;\r\n}\r\n\r\nfunction getSystemTheme() {\r\n const isDark = window.matchMedia(COLOR_SCHEME_DARK).matches;\r\n return isDark ? THEME_DARK : THEME_LIGHT;\r\n}\r\n\r\nfunction getInitialStateForIcon() {\r\n let theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) {\r\n return ICON_INITIAL_STATE_FOR_AUTO;\r\n } else if (theme === THEME_DARK) {\r\n return ICON_INITIAL_STATE_FOR_DARK;\r\n } else /* if (theme === THEME_LIGHT) */ {\r\n return ICON_INITIAL_STATE_FOR_LIGHT;\r\n }\r\n}\r\n\r\n// See https://stackoverflow.com/q/48316611\r\nfunction toggleTheme() {\r\n let theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) {\r\n localStorage.setItem(THEME_KEY, THEME_LIGHT);\r\n animateThemeButtonIconToLight();\r\n } else if (theme === THEME_DARK) {\r\n localStorage.setItem(THEME_KEY, THEME_AUTO);\r\n animateThemeButtonIconToAuto();\r\n } else /* if (theme === THEME_LIGHT) */ {\r\n localStorage.setItem(THEME_KEY, THEME_DARK);\r\n animateThemeButtonIconToDark();\r\n }\r\n updateTheme();\r\n}\r\n\r\nfunction animateThemeButtonIconToLight() {\r\n shadowRoot.getElementById(\"letter-anim-hide\").beginElement();\r\n shadowRoot.getElementById(\"core-anim-shrink\").beginElement();\r\n shadowRoot.getElementById(\"rays-anim-rotate\").beginElement();\r\n shadowRoot.getElementById(\"rays-anim-show\").beginElement();\r\n}\r\n\r\nfunction animateThemeButtonIconToAuto() {\r\n shadowRoot.getElementById(\"eclipse-anim-go\").beginElement();\r\n shadowRoot.getElementById(\"letter-anim-show\").beginElement();\r\n}\r\n\r\nfunction animateThemeButtonIconToDark() {\r\n shadowRoot.getElementById(\"rays-anim-hide\").beginElement();\r\n shadowRoot.getElementById(\"core-anim-enlarge\").beginElement();\r\n shadowRoot.getElementById(\"eclipse-anim-come\").beginElement();\r\n}\r\n\r\n// Export for tests run by npm (no longer needed; kept for future reference)\r\n// See https://stackoverflow.com/q/63752210/8583692\r\n// and https://stackoverflow.com/a/54680602/8583692\r\n// and https://stackoverflow.com/q/43042889/8583692\r\n// and https://stackoverflow.com/a/1984728/8583692\r\n// if (typeof module !== \"undefined\") {\r\n// module.exports = {\r\n// updateTheme,\r\n// toggleTheme,\r\n// getSystemTheme,\r\n// getInitialStateForIcon,\r\n// animateThemeButtonIconToAuto,\r\n// animateThemeButtonIconToDark,\r\n// animateThemeButtonIconToLight\r\n// };\r\n// }\r\n"]} \ No newline at end of file +{"version":3,"sources":["theme-switch.js"],"names":[],"mappings":"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","file":"theme-switch.min.js","sourcesContent":["/*\r\n* NOTE: Do not use this script as an ES6 module.\r\n* ES6 modules are deferred and we don't want that because\r\n* we want the user previous theme selection to be applied\r\n* as soon as possible (before the page is rendered).\r\n* */\r\n\r\n/*\r\n* There are two types of modules mostly used in JavaScript.\r\n* One is created by Node.js and is used inside the Node environment\r\n* and has been available for a long time. It is called CommonJS.\r\n* Another is the standard native JavaScript modules introduced in ES6.\r\n*\r\n* The Node variant (CommonJS) uses `module.exports` (or simply `exports`) and\r\n* `require()` to export and import scripts, functions, variables, etc.\r\n* Browsers do not know about `exports` or `require` functions and throw error\r\n* because they are objects and functions created just in Node environment and set globally.\r\n* If you want to use this type of module in browsers, you should bundle the files\r\n* (merge all of them into a single JS file which eliminates the need for exports and require)\r\n* with tools like babel, webpack, rollup, etc.\r\n*\r\n* ES6 modules use `export` and `import` keywords for the same purpose.\r\n*\r\n* Example Node modules:\r\n*\r\n* my-calculator.js\r\n* const PI = 3.14;\r\n* function calculate() {}\r\n* modules.exports.calculate = calculate;\r\n* modules.exports.PI = PI;\r\n*\r\n* main.js\r\n* const calculator = require(\"my-calculator\");\r\n* let perimeter = 2 * calculator.PI;\r\n* let result = calculator.calculate();\r\n*\r\n* Example ES6 modules:\r\n*\r\n* my-calculator.js\r\n* export const PI = 3.14;\r\n* export function calculate() {}\r\n*\r\n* main.js\r\n* import { calculate, PI } from \"my-calculator.js\";\r\n* let perimeter = 2 * PI;\r\n* let result = calculate();\r\n*\r\n* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules\r\n* and https://stackoverflow.com/a/9901097/8583692\r\n* */\r\n\r\n/*\r\n* Minify the script either through command line with babel:\r\n* - babel theme-switch.js --source-maps --out-file result.min.js\r\n* or with babel-minify (also has an alias called minify) which is useful if\r\n* you don't already use babel (as a preset) or want to run minification standalone.\r\n* Note that it does not take into account babel.config.json settings.\r\n* - babel-minify (or minify) theme-switch.js --mangle --no-comments --out-file result.min.js`\r\n* Or automate it with IntelliJ file watcher\r\n* - babel\r\n* + program: $ProjectFileDir$\\node_modules\\.bin\\babel\r\n* + arguments: $FilePathRelativeToProjectRoot$ --out-file $FileNameWithoutExtension$.min.js --source-maps\r\n* Note that setting \"sourceMaps\": \"true\" in babel.config.json does not work because of\r\n* this bug: https://github.com/babel/babel/issues/5261 (\"sourceMaps\": \"inline\" works, however)\r\n* - UglifyJS\r\n* - YUI compressor (seems to be deprecated and removed in newer versions of IntelliJ)\r\n*\r\n* Babel preset-env inserts a semicolon at the start of the minified file.\r\n* See why: https://stackoverflow.com/q/1873983/8583692\r\n* */\r\n\r\n// TODO: extract Jest configuration to a jest.config.js file\r\n\r\n/*\r\n* NOTE: To avoid name collisions if another script declares variables or functions with the same name\r\n* as ours (i.e. defining them in the global scope) and browsers complaining about identifiers\r\n* being redeclared, we wrap all our code in a closure or IIFE (sort of creating a namespace for it).\r\n* ES6 now supports block scope as well (simply wrapping the whole code in {}).\r\n* I am now using the babel-plugin-iife-wrap plugin to wrap the whole result (minified)\r\n* code in an IIFE.\r\n* For examples, see these libraries:\r\n* - https://github.com/highlightjs/highlight.js/blob/main/src/highlight.js\r\n* - https://github.com/jashkenas/underscore/blob/master/underscore.js\r\n* See\r\n* - https://www.w3schools.com/js/js_scope.asp\r\n* - https://github.com/jhnns/rewire/issues/136#issuecomment-380829197\r\n* - https://stackoverflow.com/a/32750216/8583692\r\n* - https://stackoverflow.com/q/8228281/8583692\r\n* - https://stackoverflow.com/q/881515/8583692\r\n* - https://stackoverflow.com/q/39388777/8583692\r\n* - https://stackoverflow.com/a/47207686/8583692\r\n* We could also do something like these libraries:\r\n* - https://github.com/juliangarnier/anime/blob/master/build.js\r\n* - https://github.com/mrdoob/three.js/\r\n* - https://github.com/moment/moment\r\n* - https://github.com/floating-ui/floating-ui\r\n* */\r\n\r\nconst ICON_SIZE = 24 /* px */;\r\nconst THEME_KEY = \"theme\";\r\nconst THEME_AUTO = \"auto\";\r\nconst THEME_DARK = \"dark\";\r\nconst THEME_LIGHT = \"light\";\r\nconst THEME_DEFAULT = THEME_LIGHT;\r\nconst THEME_ATTRIBUTE = \"data-theme\";\r\nconst COLOR_SCHEME_DARK = \"(prefers-color-scheme: dark)\";\r\n\r\n// circleRadius, raysOpacity, eclipseCenterX, letterOffset\r\nconst ICON_INITIAL_STATE_FOR_AUTO = [10, 0, 33, 0];\r\nconst ICON_INITIAL_STATE_FOR_DARK = [10, 0, 20, 1];\r\nconst ICON_INITIAL_STATE_FOR_LIGHT = [5, 1, 33, 1];\r\n\r\nlet shadowRoot;\r\n\r\nclass ThemeSwitchElement extends HTMLElement {\r\n constructor() {\r\n super();\r\n\r\n // See https://stackoverflow.com/q/2305654/8583692\r\n shadowRoot = this.attachShadow({mode: \"open\"});\r\n shadowRoot.innerHTML = generateIcon(...getInitialStateForIcon());\r\n\r\n // Add the click listener to the top-most parent (the custom element itself)\r\n // so the padding etc. on the element be also clickable\r\n shadowRoot.host.addEventListener(\"click\", toggleTheme);\r\n\r\n // Create some CSS to apply to the shadow DOM\r\n // See https://css-tricks.com/styling-a-web-component/\r\n const style = document.createElement(\"style\");\r\n style.textContent = generateStyle();\r\n shadowRoot.append(style);\r\n }\r\n}\r\n\r\n// language=html\r\nfunction generateIcon(circleRadius, raysOpacity, eclipseCenterX, letterOffset) {\r\n return `\r\n \r\n \r\n `;\r\n}\r\n\r\n// language=css\r\nfunction generateStyle() {\r\n return `\r\n /* :host === the host element of the shadow === */\r\n /* See https://developer.mozilla.org/en-US/docs/Web/CSS/:host */\r\n :host {\r\n display: flex;\r\n width: ${ICON_SIZE}px;\r\n aspect-ratio: 1 / 1;\r\n /* This is for when the element has padding */\r\n cursor: pointer;\r\n }\r\n\r\n button {\r\n padding: 0;\r\n border: none;\r\n background: transparent;\r\n display: flex;\r\n /* The host element also has its cursor set */\r\n cursor: pointer;\r\n }\r\n\r\n #circle { fill: var(--theme-switch-icon-color, #000); }\r\n\r\n #rays { stroke: var(--theme-switch-icon-color, #000); }\r\n `;\r\n}\r\n\r\nupdateTheme();\r\nwindow.customElements.define(\"theme-switch\", ThemeSwitchElement);\r\nwindow\r\n .matchMedia(COLOR_SCHEME_DARK)\r\n .addEventListener(\"change\", updateTheme);\r\n\r\nfunction updateTheme() {\r\n let theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) theme = getSystemTheme();\r\n document.documentElement.setAttribute(THEME_ATTRIBUTE, theme);\r\n}\r\n\r\nfunction getUserThemeSelection() {\r\n const userSelection = localStorage.getItem(THEME_KEY);\r\n return userSelection === null ? THEME_DEFAULT : userSelection;\r\n}\r\n\r\nfunction getSystemTheme() {\r\n const isDark = window.matchMedia(COLOR_SCHEME_DARK).matches;\r\n return isDark ? THEME_DARK : THEME_LIGHT;\r\n}\r\n\r\nfunction getInitialStateForIcon() {\r\n let theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) {\r\n return ICON_INITIAL_STATE_FOR_AUTO;\r\n } else if (theme === THEME_DARK) {\r\n return ICON_INITIAL_STATE_FOR_DARK;\r\n } else /* if (theme === THEME_LIGHT) */ {\r\n return ICON_INITIAL_STATE_FOR_LIGHT;\r\n }\r\n}\r\n\r\n// See https://stackoverflow.com/q/48316611\r\nfunction toggleTheme() {\r\n let theme = getUserThemeSelection();\r\n if (theme === THEME_AUTO) {\r\n localStorage.setItem(THEME_KEY, THEME_LIGHT);\r\n animateThemeButtonIconToLight();\r\n } else if (theme === THEME_DARK) {\r\n localStorage.setItem(THEME_KEY, THEME_AUTO);\r\n animateThemeButtonIconToAuto();\r\n } else /* if (theme === THEME_LIGHT) */ {\r\n localStorage.setItem(THEME_KEY, THEME_DARK);\r\n animateThemeButtonIconToDark();\r\n }\r\n updateTheme();\r\n}\r\n\r\nfunction animateThemeButtonIconToLight() {\r\n shadowRoot.getElementById(\"letter-anim-hide\").beginElement();\r\n shadowRoot.getElementById(\"core-anim-shrink\").beginElement();\r\n shadowRoot.getElementById(\"rays-anim-rotate\").beginElement();\r\n shadowRoot.getElementById(\"rays-anim-show\").beginElement();\r\n}\r\n\r\nfunction animateThemeButtonIconToAuto() {\r\n shadowRoot.getElementById(\"eclipse-anim-go\").beginElement();\r\n shadowRoot.getElementById(\"letter-anim-show\").beginElement();\r\n}\r\n\r\nfunction animateThemeButtonIconToDark() {\r\n shadowRoot.getElementById(\"rays-anim-hide\").beginElement();\r\n shadowRoot.getElementById(\"core-anim-enlarge\").beginElement();\r\n shadowRoot.getElementById(\"eclipse-anim-come\").beginElement();\r\n}\r\n\r\n// Export for tests run by npm (no longer needed; kept for future reference)\r\n// See https://stackoverflow.com/q/63752210/8583692\r\n// and https://stackoverflow.com/a/54680602/8583692\r\n// and https://stackoverflow.com/q/43042889/8583692\r\n// and https://stackoverflow.com/a/1984728/8583692\r\n// if (typeof module !== \"undefined\") {\r\n// module.exports = {\r\n// updateTheme,\r\n// toggleTheme,\r\n// getSystemTheme,\r\n// getInitialStateForIcon,\r\n// animateThemeButtonIconToAuto,\r\n// animateThemeButtonIconToDark,\r\n// animateThemeButtonIconToLight\r\n// };\r\n// }\r\n"]} \ No newline at end of file diff --git a/theme-switch.test.js b/theme-switch.test.js index 678202b..dac1d57 100644 --- a/theme-switch.test.js +++ b/theme-switch.test.js @@ -196,10 +196,34 @@ describe("Screenshot tests", () => { expect(snapshotTakenNow).toMatchReferenceSnapshot(); }, 100_000); + test(`When user specifies a custom color for switch icon, the colors should be applied`, async () => { + await takeScreenshot( + () => {localStorage.setItem("theme", "light");}, + element => { + // See https://stackoverflow.com/a/64487791/8583692 + element.evaluate((el) => { + el.style.setProperty("--theme-switch-icon-color", "#ffe36e"); + }); + } + ); + const snapshotTakenNow = fileSystem.readFileSync(snapshotFileName); + expect(snapshotTakenNow).toMatchReferenceSnapshot(); + }, 100_000); + + test(`When user specifies a custom color for switch icon in a CSS rule with low specificity (like html{}), the colors should be applied`, async () => { + await takeScreenshot( + () => {localStorage.setItem("theme", "light");}, + (element) => {}, + "test2.html" + ); + const snapshotTakenNow = fileSystem.readFileSync(snapshotFileName); + expect(snapshotTakenNow).toMatchReferenceSnapshot(); + }, 100_000); + afterAll(() => {fileSystem.rmSync(snapshotFileName);}); }); -async function takeScreenshot(init, action = () => {}) { +async function takeScreenshot(init, action = () => {}, pageHTML = "test.html") { const browser = await puppeteer.launch({ headless: true, // If false, opens the browser UI // channel: "chrome", // this overrides executablePath @@ -213,11 +237,11 @@ async function takeScreenshot(init, action = () => {}) { // See https://stackoverflow.com/a/66530593/8583692 await page.evaluateOnNewDocument(init); // page.setContent("...") - await page.goto(`file://${__dirname}\\test.html`); + await page.goto(`file://${__dirname}\\${pageHTML}`); const element = await page.$("theme-switch"); await action(element); - // Wait for the element animation to finish + // Wait for the action or element animation to finish await page.waitForTimeout(1000); await element.screenshot({path: snapshotFileName});