diff --git a/flow-server/src/main/resources/plugins/application-theme-plugin/theme-generator.js b/flow-server/src/main/resources/plugins/application-theme-plugin/theme-generator.js index 5d129495ea2..0dc204b5d27 100644 --- a/flow-server/src/main/resources/plugins/application-theme-plugin/theme-generator.js +++ b/flow-server/src/main/resources/plugins/application-theme-plugin/theme-generator.js @@ -174,7 +174,7 @@ function generateThemeFile(themeFolder, themeName, themeProperties, productionMo } themeProperties.documentCss.forEach((cssImport) => { const variable = 'module' + i++; - imports.push(`import ${variable} from '${cssImport}';\n`); + imports.push(`import ${variable} from '${cssImport}?inline';\n`); // Due to chrome bug https://bugs.chromium.org/p/chromium/issues/detail?id=336876 font-face will not work // inside shadowRoot so we need to inject it there also. globalCssCode.push(`if(target !== document) { diff --git a/flow-server/src/main/resources/plugins/theme-loader/package.json b/flow-server/src/main/resources/plugins/theme-loader/package.json index 33ff966f4c6..c44a301fce7 100644 --- a/flow-server/src/main/resources/plugins/theme-loader/package.json +++ b/flow-server/src/main/resources/plugins/theme-loader/package.json @@ -14,6 +14,7 @@ "url": "https://github.com/vaadin/flow/issues" }, "files": [ - "theme-loader.js" + "theme-loader.js", + "theme-loader-utils.js" ] } diff --git a/flow-server/src/main/resources/plugins/theme-loader/theme-loader-utils.js b/flow-server/src/main/resources/plugins/theme-loader/theme-loader-utils.js new file mode 100644 index 00000000000..7668c459403 --- /dev/null +++ b/flow-server/src/main/resources/plugins/theme-loader/theme-loader-utils.js @@ -0,0 +1,83 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +// Collect groups [url(] ['|"]optional './|../', file part and end of url +const urlMatcher = /(url\(\s*)(\'|\")?(\.\/|\.\.\/)(\S*)(\2\s*\))/g; + + +function assetsContains(fileUrl, themeFolder, logger) { + const themeProperties = getThemeProperties(themeFolder); + if (!themeProperties) { + logger.debug('No theme properties found.'); + return false; + } + const assets = themeProperties['assets']; + if (!assets) { + logger.debug('No defined assets in theme properties'); + return false; + } + // Go through each asset module + for (let module of Object.keys(assets)) { + const copyRules = assets[module]; + logger.log('asset ' + module); + // Go through each copy rule + for (let copyRule of Object.keys(copyRules)) { + logger.log('rule ' + copyRules[copyRule] + " ---> file " + fileUrl); + // if file starts with copyRule target check if file with path after copy target can be found + if (fileUrl.startsWith(copyRules[copyRule])) { + const targetFile = fileUrl.replace(copyRules[copyRule], ''); + const files = glob.sync(path.resolve('node_modules/', module, copyRule), { nodir: true }); + + logger.log('targetFile ' + targetFile); + for (let file of files) { + if (file.endsWith(targetFile)) return true; + } + } + } + } + return false; +} + +function getThemeProperties(themeFolder) { + const themePropertyFile = path.resolve(themeFolder, 'theme.json'); + if (!fs.existsSync(themePropertyFile)) { + return {}; + } + const themePropertyFileAsString = fs.readFileSync(themePropertyFile); + if (themePropertyFileAsString.length === 0) { + return {}; + } + return JSON.parse(themePropertyFileAsString); +} + + +function rewriteCssUrls(source, handledResourceFolder, themeFolder, logger, options) { + source = source.replace(urlMatcher, function (match, url, quoteMark, replace, fileUrl, endString) { + let absolutePath = path.resolve(handledResourceFolder, replace, fileUrl); + const existingThemeResource = absolutePath.startsWith(themeFolder) && fs.existsSync(absolutePath); + if ( + existingThemeResource || assetsContains(fileUrl, themeFolder, logger) + ) { + // Adding ./ will skip css-loader, which should be done for asset files + const skipLoader = existingThemeResource ? '' : './'; + const frontendThemeFolder = skipLoader + 'themes/' + path.basename(themeFolder); + logger.debug( + 'Updating url for file', + "'" + replace + fileUrl + "'", + 'to use', + "'" + frontendThemeFolder + '/' + fileUrl + "'" + ); + const pathResolved = absolutePath.substring(themeFolder.length).replace(/\\/g, '/'); + + // keep the url the same except replace the ./ or ../ to themes/[themeFolder] + return url + (quoteMark??'') + frontendThemeFolder + pathResolved + endString; + } else if (options.devMode) { + logger.log("No rewrite for '", match, "' as the file was not found."); + } + return match; + }); + return source; +} + +module.exports = { rewriteCssUrls }; diff --git a/flow-server/src/main/resources/plugins/theme-loader/theme-loader.js b/flow-server/src/main/resources/plugins/theme-loader/theme-loader.js index 3ce3395a083..9bb6b89861e 100644 --- a/flow-server/src/main/resources/plugins/theme-loader/theme-loader.js +++ b/flow-server/src/main/resources/plugins/theme-loader/theme-loader.js @@ -1,9 +1,7 @@ const loaderUtils = require('loader-utils'); const fs = require('fs'); const path = require('path'); - -// Collect groups [url(] [ |'|"]optional './|../', file part and end of url -const urlMatcher = /(url\()(\'|\")?(\.\/|\.\.\/)(\S*)(\2\))/g; +const { rewriteCssUrls } = require('./theme-loader-utils'); /** * This custom loader handles rewriting urls for the application theme css files. @@ -31,75 +29,6 @@ module.exports = function (source, map) { logger.log("Using '", themeFolder, "' for the application theme base folder."); - source = source.replace(urlMatcher, function (match, url, quoteMark, replace, fileUrl, endString) { - let absolutePath = path.resolve(handledResourceFolder, replace, fileUrl); - if ( - (fs.existsSync(absolutePath) && absolutePath.startsWith(themeFolder)) || - assetsContains(fileUrl, themeFolder, logger) - ) { - // Adding ./ will skip css-loader, which should be done for asset files - const skipLoader = fs.existsSync(absolutePath) && absolutePath.startsWith(themeFolder) ? '' : './'; - const frontendThemeFolder = skipLoader + 'themes/' + path.basename(themeFolder); - logger.debug( - 'Updating url for file', - "'" + replace + fileUrl + "'", - 'to use', - "'" + frontendThemeFolder + '/' + fileUrl + "'" - ); - const pathResolved = absolutePath.substring(themeFolder.length).replace(/\\/g, '/'); - - // keep the url the same except replace the ./ or ../ to themes/[themeFolder] - if (quoteMark) { - return url + quoteMark + frontendThemeFolder + pathResolved + endString; - } - return url + frontendThemeFolder + pathResolved + endString; - } else if (options.devMode) { - logger.log("No rewrite for '", match, "' as the file was not found."); - } - return match; - }); - + source = rewriteCssUrls(source, handledResourceFolder, themeFolder, logger, options); this.callback(null, source, map); }; - -function assetsContains(fileUrl, themeFolder, logger) { - const themeProperties = getThemeProperties(themeFolder); - if (!themeProperties) { - logger.debug('No theme properties found.'); - return false; - } - const assets = themeProperties['assets']; - if (!assets) { - logger.debug('No defined assets in theme properties'); - return false; - } - // Go through each asset module - for (let module of Object.keys(assets)) { - const copyRules = assets[module]; - // Go through each copy rule - for (let copyRule of Object.keys(copyRules)) { - // if file starts with copyRule target check if file with path after copy target can be found - if (fileUrl.startsWith(copyRules[copyRule])) { - const targetFile = fileUrl.replace(copyRules[copyRule], ''); - const files = require('glob').sync(path.resolve('node_modules/', module, copyRule), { nodir: true }); - - for (let file of files) { - if (file.endsWith(targetFile)) return true; - } - } - } - } - return false; -} - -function getThemeProperties(themeFolder) { - const themePropertyFile = path.resolve(themeFolder, 'theme.json'); - if (!fs.existsSync(themePropertyFile)) { - return {}; - } - const themePropertyFileAsString = fs.readFileSync(themePropertyFile); - if (themePropertyFileAsString.length === 0) { - return {}; - } - return JSON.parse(themePropertyFileAsString); -} diff --git a/flow-server/src/main/resources/vite.generated.ts b/flow-server/src/main/resources/vite.generated.ts index c10b423a99c..ca4a10cff34 100644 --- a/flow-server/src/main/resources/vite.generated.ts +++ b/flow-server/src/main/resources/vite.generated.ts @@ -9,6 +9,7 @@ import { readFileSync, existsSync, writeFileSync } from 'fs'; import * as net from 'net'; import { processThemeResources } from '#buildFolder#/plugins/application-theme-plugin/theme-handle'; +import { rewriteCssUrls } from '#buildFolder#/plugins/theme-loader/theme-loader-utils'; import settings from '#settingsImport#'; import { defineConfig, mergeConfig, PluginOption, ResolvedConfig, UserConfigFn, OutputOptions, AssetInfo, ChunkInfo } from 'vite'; import { injectManifest } from 'workbox-build'; @@ -305,7 +306,7 @@ export { ${exports.map((binding) => `${binding} as ${binding}`).join(', ')} };`; }; } -function themePlugin(): PluginOption { +function themePlugin(opts): PluginOption { return { name: 'vaadin:theme', config() { @@ -336,6 +337,15 @@ function themePlugin(): PluginOption { } } }, + async transform(raw, id, options) { + // rewrite urls for the application theme css files + const [bareId, query] = id.split('?'); + if (!bareId?.startsWith(themeFolder) || !bareId?.endsWith(".css")) { + return; + } + const [themeName] = bareId.substring(themeFolder.length + 1).split('/'); + return rewriteCssUrls(raw, path.dirname(bareId), path.resolve(themeFolder, themeName), console, opts); + } } } @@ -361,7 +371,7 @@ const allowedFrontendFolders = [ frontendFolder, addonFrontendFolder, path.resolve(addonFrontendFolder, '..', 'frontend'), // Contains only generated-flow-imports - path.resolve(frontendFolder, '../node_modules') + path.resolve(__dirname, 'node_modules') ]; export const vaadinConfig: UserConfigFn = (env) => { @@ -374,7 +384,7 @@ export const vaadinConfig: UserConfigFn = (env) => { } return { - root: 'frontend', + root: frontendFolder, base: '', resolve: { alias: { @@ -422,7 +432,7 @@ export const vaadinConfig: UserConfigFn = (env) => { settings.offlineEnabled && buildSWPlugin(), settings.offlineEnabled && injectManifestToSWPlugin(), !devMode && statsExtracterPlugin(), - themePlugin(), + themePlugin({devMode}), { name: 'vaadin:force-remove-spa-middleware', transformIndexHtml: { diff --git a/flow-tests/test-custom-frontend-directory/pom.xml b/flow-tests/test-custom-frontend-directory/pom.xml index 5596b26d388..5934ebc2d9a 100644 --- a/flow-tests/test-custom-frontend-directory/pom.xml +++ b/flow-tests/test-custom-frontend-directory/pom.xml @@ -24,6 +24,7 @@ test-themes-custom-frontend-directory/pom.xml test-themes-custom-frontend-directory/pom-generatedTsDir.xml + test-themes-custom-frontend-directory/pom-vite.xml diff --git a/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/pom-vite.xml b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/pom-vite.xml new file mode 100644 index 00000000000..e80c9e2cb3b --- /dev/null +++ b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/pom-vite.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + com.vaadin + test-custom-frontend-directory + 23.1-SNAPSHOT + + flow-test-themes-custom-frontend-directory-vite + Flow themes tests in Vite with custom frontend directory + war + + true + true + false + + + + + com.vaadin + flow-test-common + ${project.version} + + + com.vaadin + flow-html-components-testbench + ${project.version} + test + + + com.vaadin + vaadin-dev-server + ${project.version} + + + com.vaadin + flow-test-lumo + ${project.version} + + + + + + + ${project.basedir}/src/main/vite-resources + + + ${project.basedir}/src/test-vite/java + + + org.eclipse.jetty + jetty-maven-plugin + + + com.vaadin + flow-maven-plugin + + ${project.basedir}/side-src/main/frontend + false + + + + ensure-fronted-deleted + + clean-frontend + + initialize + + + + + org.apache.maven.plugins + maven-clean-plugin + + + ensure-fronted-deleted + + clean + + initialize + + ${project.basedir}/frontend + false + + + + + + + diff --git a/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/main/vite-resources/vaadin-featureflags.properties b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/main/vite-resources/vaadin-featureflags.properties new file mode 100644 index 00000000000..e4d770f9a4b --- /dev/null +++ b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/main/vite-resources/vaadin-featureflags.properties @@ -0,0 +1 @@ +com.vaadin.experimental.viteForFrontendBuild=true diff --git a/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test-vite/java/com/vaadin/flow/uitest/ui/theme/CssLoadingIT.java b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test-vite/java/com/vaadin/flow/uitest/ui/theme/CssLoadingIT.java new file mode 100644 index 00000000000..04fb93516e7 --- /dev/null +++ b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test-vite/java/com/vaadin/flow/uitest/ui/theme/CssLoadingIT.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.uitest.ui.theme; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.flow.component.html.testbench.ParagraphElement; +import com.vaadin.flow.testutil.ChromeBrowserTest; + +/** + * Test CSS loading order from different sources. + * + * The expected priority is: Lumo styles < @CssImport < page.addStylesheet + * < @Stylehseet < parent theme < current theme (app theme) + */ +public class CssLoadingIT extends ChromeBrowserTest { + + private static final String BLUE_RGBA = "rgba(0, 0, 255, 1)"; + private static final String GREEN_RGBA = "rgba(0, 255, 0, 1)"; + private static final String YELLOW_RGBA = "rgba(255, 255, 0, 1)"; + private static final String STYLESHEET_LUMO_FONT_SIZE_M = " 1.1rem"; + + @Test + public void CssImport_overrides_Lumo() { + open(); + WebElement htmlElement = findElement(By.tagName("html")); + + Assert.assertEquals("CssImport styles should override Lumo styles.", + STYLESHEET_LUMO_FONT_SIZE_M, + executeScript( + "return getComputedStyle(arguments[0]).getPropertyValue('--lumo-font-size-m')", + htmlElement)); + } + + @Test + public void multipleDefinitions_correctOverrides() { + open(); + assertStylesOverride("p1", GREEN_RGBA, "16px", "1px"); + + // @Stylesheet should override color and font-size but not margin + assertStylesOverride("p2", BLUE_RGBA, "18px", "1px"); + + assertStylesOverride("p3", YELLOW_RGBA, "20px", "2px"); + } + + private void assertStylesOverride(String elementId, String expectedColor, + String expectedFontSize, String expectedMargin) { + Assert.assertEquals(expectedColor, + $(ParagraphElement.class).id(elementId).getCssValue("color")); + Assert.assertEquals(expectedFontSize, $(ParagraphElement.class) + .id(elementId).getCssValue("font-size")); + Assert.assertEquals(expectedMargin, + $(ParagraphElement.class).id(elementId).getCssValue("margin")); + } +} diff --git a/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test-vite/java/com/vaadin/flow/uitest/ui/theme/ThemeIT.java b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test-vite/java/com/vaadin/flow/uitest/ui/theme/ThemeIT.java new file mode 100644 index 00000000000..fcc95b0a4e0 --- /dev/null +++ b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test-vite/java/com/vaadin/flow/uitest/ui/theme/ThemeIT.java @@ -0,0 +1,327 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.uitest.ui.theme; + +import java.io.File; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebElement; + +import com.vaadin.experimental.FeatureFlags; +import com.vaadin.flow.component.html.testbench.ImageElement; +import com.vaadin.flow.component.html.testbench.SpanElement; +import com.vaadin.flow.testutil.ChromeBrowserTest; +import com.vaadin.testbench.TestBenchElement; + +import static com.vaadin.flow.uitest.ui.theme.ThemeView.BUTTERFLY_ID; +import static com.vaadin.flow.uitest.ui.theme.ThemeView.CSS_SNOWFLAKE; +import static com.vaadin.flow.uitest.ui.theme.ThemeView.DICE_ID; +import static com.vaadin.flow.uitest.ui.theme.ThemeView.FONTAWESOME_ID; +import static com.vaadin.flow.uitest.ui.theme.ThemeView.MY_COMPONENT_ID; +import static com.vaadin.flow.uitest.ui.theme.ThemeView.OCTOPUSS_ID; +import static com.vaadin.flow.uitest.ui.theme.ThemeView.SNOWFLAKE_ID; +import static com.vaadin.flow.uitest.ui.theme.ThemeView.SUB_COMPONENT_ID; + +public class ThemeIT extends ChromeBrowserTest { + + @Ignore + @Test + public void typeScriptCssImport_stylesAreApplied() { + getDriver().get(getRootURL() + "/path/hello"); + waitForDevServer(); + + checkLogsForErrors(); + + final TestBenchElement helloWorld = $(TestBenchElement.class).first() + .findElement(By.tagName("hello-world-view")); + + Assert.assertEquals("hello-world-view", helloWorld.getTagName()); + + Assert.assertEquals( + "CSS was not applied as background color was not as expected.", + "rgba(255, 165, 0, 1)", + helloWorld.getCssValue("background-color")); + } + + @Test + public void referenceResourcesOnJavaSideForStyling_stylesAreApplied() { + open(); + final String resourceUrl = getRootURL() + + "/path/themes/app-theme/img/dice.jpg"; + WebElement diceSpan = findElement(By.id(DICE_ID)); + final String expectedImgUrl = "url(\"" + resourceUrl + "\")"; + Assert.assertEquals( + "Background image has been referenced on java page and " + + "expected to be applied", + expectedImgUrl, diceSpan.getCssValue("background-image")); + getDriver().get(resourceUrl); + Assert.assertFalse("Java-side referenced resource should be served", + driver.getPageSource().contains("HTTP ERROR 404 Not Found")); + } + + @Test + public void nodeAssetInCss_pathIsSetCorrectly() { + open(); + final String resourceUrl = getRootURL() + + "/path/themes/app-theme/fortawesome/icons/snowflake.svg"; + WebElement cssNodeSnowflake = findElement(By.id(CSS_SNOWFLAKE)); + final String expectedImgUrl = "url(\"" + resourceUrl + "\")"; + Assert.assertEquals( + "Background image has been referenced in styles.css and " + + "expected to be applied", + expectedImgUrl, + cssNodeSnowflake.getCssValue("background-image")); + } + + @Test + public void secondTheme_staticFilesNotCopied() { + getDriver().get(getRootURL() + "/path/themes/app-theme/img/bg.jpg"); + Assert.assertFalse("app-theme static files should be copied", + driver.getPageSource().contains("HTTP ERROR 404 Not Found")); + + getDriver().get(getRootURL() + "/path/themes/no-copy/no-copy.txt"); + String source = driver.getPageSource(); + Matcher m = Pattern.compile( + ".*Could not navigate to.*themes/no-copy/no-copy.txt.*", + Pattern.DOTALL).matcher(source); + Assert.assertTrue("no-copy theme should not be handled", m.matches()); + } + + @Test + public void applicationTheme_onlyStylesCssIsApplied() { + open(); + // No exception for bg-image should exist + checkLogsForErrors(); + + // Vite ignores servlet path and assumes servlet with custom mapping + // also covers /VAADIN/* + + final WebElement body = findElement(By.tagName("body")); + Assert.assertEquals("body background-image should come from styles.css", + "url(\"" + getRootURL() + + "/VAADIN/themes/app-theme/img/bg.jpg\")", + body.getCssValue("background-image")); + + Assert.assertEquals("body font-family should come from styles.css", + "Ostrich", body.getCssValue("font-family")); + + Assert.assertEquals("html color from styles.css should be applied.", + "rgba(0, 0, 0, 1)", body.getCssValue("color")); + + getDriver().get(getRootURL() + "/VAADIN/themes/app-theme/img/bg.jpg"); + Assert.assertFalse("app-theme background file should be served", + driver.getPageSource().contains("Could not navigate")); + } + + @Test + public void applicationTheme_importCSS_isUsed() { + open(); + checkLogsForErrors(); + + Assert.assertEquals("Imported FontAwesome css file should be applied.", + "\"Font Awesome 5 Free\"", $(SpanElement.class) + .id(FONTAWESOME_ID).getCssValue("font-family")); + + String iconUnicode = getCssPseudoElementValue(FONTAWESOME_ID, + "::before"); + Assert.assertEquals( + "Font-Icon from FontAwesome css file should be applied.", + "\"\uf0f4\"", iconUnicode); + + getDriver().get(getRootURL() + + "/path/VAADIN/static/@fortawesome/fontawesome-free/webfonts/fa-solid-900.svg"); + Assert.assertFalse("Font resource should be available", + driver.getPageSource().contains("HTTP ERROR 404 Not Found")); + } + + @Test + public void parentTheme_isApplied() { + open(); + checkLogsForErrors(); + + Assert.assertEquals("Color from parent theme should be applied.", + "rgba(0, 255, 255, 1)", + $(SpanElement.class).id(FONTAWESOME_ID).getCssValue("color")); + + Assert.assertEquals("Child theme should override parent theme values", + "5px", + $(SpanElement.class).id(FONTAWESOME_ID).getCssValue("margin")); + + Assert.assertEquals("Child theme values should be applied", "5px", + $(SpanElement.class).id(FONTAWESOME_ID).getCssValue("padding")); + + TestBenchElement myField = $(TestBenchElement.class) + .id(MY_COMPONENT_ID); + + TestBenchElement input = myField.$("vaadin-input-container") + .attribute("part", "input-field").first(); + Assert.assertEquals( + "Polymer text field should get parent border radius", "0px", + input.getCssValue("border-radius")); + + Assert.assertEquals("Polymer text field should use green as color", + "rgba(0, 128, 0, 1)", input.getCssValue("color")); + + } + + @Test + public void componentThemeIsApplied() { + open(); + TestBenchElement myField = $(TestBenchElement.class) + .id(MY_COMPONENT_ID); + TestBenchElement input = myField.$("vaadin-input-container") + .attribute("part", "input-field").first(); + Assert.assertEquals("Polymer text field should have red background", + "rgba(255, 0, 0, 1)", input.getCssValue("background-color")); + } + + @Test + public void subCssWithRelativePath_urlPathIsNotRelative() { + open(); + checkLogsForErrors(); + + // Vite ignores servlet path and assumes servlet with custom mapping + // also covers /VAADIN/* + Assert.assertEquals("Imported css file URLs should have been handled.", + "url(\"" + getRootURL() + + "/VAADIN/themes/app-theme/icons/archive.png\")", + $(SpanElement.class).id(SUB_COMPONENT_ID) + .getCssValue("background-image")); + } + + @Test + public void staticModuleAsset_servedFromAppTheme() { + open(); + checkLogsForErrors(); + + Assert.assertEquals( + "Node assets should have been copied to 'themes/app-theme'", + getRootURL() + + "/path/themes/app-theme/fortawesome/icons/snowflake.svg", + $(ImageElement.class).id(SNOWFLAKE_ID).getAttribute("src")); + + open(getRootURL() + "/path/" + + $(ImageElement.class).id(SNOWFLAKE_ID).getAttribute("src")); + Assert.assertFalse("Node static icon should be available", + driver.getPageSource().contains("HTTP ERROR 404 Not Found")); + } + + @Test + public void nonThemeDependency_urlIsNotRewritten() { + open(); + checkLogsForErrors(); + + Assert.assertEquals("Relative non theme url should not be touched", + "url(\"" + getRootURL() + + "/path/test/path/monarch-butterfly.jpg\")", + $(SpanElement.class).id(BUTTERFLY_ID) + .getCssValue("background-image")); + + Assert.assertEquals("Absolute non theme url should not be touched", + "url(\"" + getRootURL() + "/octopuss.jpg\")", + $(SpanElement.class).id(OCTOPUSS_ID) + .getCssValue("background-image")); + + getDriver().get(getRootURL() + "/path/test/path/monarch-butterfly.jpg"); + Assert.assertFalse("webapp resource should be served", + driver.getPageSource().contains("HTTP ERROR 404 Not Found")); + + getDriver().get(getRootURL() + "/octopuss.jpg"); + Assert.assertFalse("root resource should be served", + driver.getPageSource().contains("HTTP ERROR 404 Not Found")); + } + + @Test + public void themeRulesOverrideLumo() { + open(); + checkLogsForErrors(); + Assert.assertEquals( + "Background should be blue, as overridden in the theme", + "rgba(0, 0, 255, 1)", + $("html").first().getCssValue("background-color")); + + } + + @Test + public void customFrontendDirectory_generatedFilesNotInDefaultFrontendFolder() { + open(); + + File baseDir = new File(System.getProperty("user.dir", ".")); + File expectedGeneratedFolder = new File(baseDir, + "side-src/main/frontend/generated"); + File defaultGeneratedFolder = new File(baseDir, "frontend/generated"); + + String[] generatedFiles = { "theme.d.ts", "theme.js", + "theme-app-theme.generated.js", + "theme-parent-theme.generated.js", "vaadin.ts" }; + for (String generatedFile : generatedFiles) { + Assert.assertTrue( + "Expecting " + generatedFile + " to be present in " + + expectedGeneratedFolder.getPath() + + ", but was not", + new File(expectedGeneratedFolder, generatedFile).exists()); + Assert.assertFalse( + "Expecting " + generatedFile + " not to be present in " + + defaultGeneratedFolder.getPath() + + ", but was not", + new File(defaultGeneratedFolder, generatedFile).exists()); + } + } + + @Test + public void documentCssImport_onlyExternalAddedToHeadAsLink() { + open(); + checkLogsForErrors(); + + final WebElement documentHead = getDriver() + .findElement(By.xpath("/html/head")); + final List links = documentHead + .findElements(By.tagName("link")); + + List linkUrls = links.stream() + .map(link -> link.getAttribute("href")) + .collect(Collectors.toList()); + + Assert.assertTrue("Missing link for external url", linkUrls + .contains("https://fonts.googleapis.com/css?family=Itim")); + Assert.assertFalse("Found import that webpack should have resolved", + linkUrls.contains("sub-css/sub.css")); + } + + @Override + protected String getTestPath() { + String path = super.getTestPath(); + String view = "view/"; + return path.replace(view, "path/"); + } + + private String getCssPseudoElementValue(String elementId, + String pseudoElement) { + String script = "return window.getComputedStyle(" + + "document.getElementById(arguments[0])" + + ", arguments[1]).content"; + JavascriptExecutor js = (JavascriptExecutor) driver; + return (String) js.executeScript(script, elementId, pseudoElement); + } +} diff --git a/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test/java/com/vaadin/flow/uitest/ui/theme/ThemeIT.java b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test/java/com/vaadin/flow/uitest/ui/theme/ThemeIT.java index 11c3fb963aa..7818a3a50c8 100644 --- a/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test/java/com/vaadin/flow/uitest/ui/theme/ThemeIT.java +++ b/flow-tests/test-custom-frontend-directory/test-themes-custom-frontend-directory/src/test/java/com/vaadin/flow/uitest/ui/theme/ThemeIT.java @@ -20,7 +20,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.junit.Assert; import org.junit.Test;