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;