16 | * The tests use Chrome driver (see pom.xml for integration-tests profile) to 17 | * run integration tests on a headless Chrome. If a property {@code test.use 18 | * .hub} is set to true, {@code AbstractViewTest} will assume that the 19 | * TestBench test is running in a CI environment. In order to keep the this 20 | * class light, it makes certain assumptions about the CI environment (such 21 | * as available environment variables). It is not advisable to use this class 22 | * as a base class for you own TestBench tests. 23 | *
24 | * To learn more about TestBench, visit
25 | * Vaadin TestBench.
26 | */
27 | public abstract class AbstractViewTest extends ParallelTest {
28 | private static final int SERVER_PORT = 8080;
29 |
30 | private final String route;
31 |
32 | @Rule
33 | public ScreenshotOnFailureRule rule = new ScreenshotOnFailureRule(this,
34 | true);
35 |
36 | @BeforeClass
37 | public static void setupClass() {
38 | WebDriverManager.chromedriver().setup();
39 | }
40 |
41 | public AbstractViewTest() {
42 | this("");
43 | }
44 |
45 | protected AbstractViewTest(String route) {
46 | this.route = route;
47 | }
48 |
49 | @Before
50 | public void setup() throws Exception {
51 | if (isUsingHub()) {
52 | super.setup();
53 | } else {
54 | setDriver(TestBench.createDriver(new ChromeDriver()));
55 | }
56 | getDriver().get(getURL(route));
57 | }
58 |
59 | /**
60 | * Returns deployment host name concatenated with route.
61 | *
62 | * @return URL to route
63 | */
64 | private static String getURL(String route) {
65 | return String.format("http://%s:%d/%s", getDeploymentHostname(),
66 | SERVER_PORT, route);
67 | }
68 |
69 | /**
70 | * Property set to true when running on a test hub.
71 | */
72 | private static final String USE_HUB_PROPERTY = "test.use.hub";
73 |
74 | /**
75 | * Returns whether we are using a test hub. This means that the starter
76 | * is running tests in Vaadin's CI environment, and uses TestBench to
77 | * connect to the testing hub.
78 | *
79 | * @return whether we are using a test hub
80 | */
81 | private static boolean isUsingHub() {
82 | return Boolean.TRUE.toString().equals(
83 | System.getProperty(USE_HUB_PROPERTY));
84 | }
85 |
86 | /**
87 | * If running on CI, get the host name from environment variable HOSTNAME
88 | *
89 | * @return the host name
90 | */
91 | private static String getDeploymentHostname() {
92 | return isUsingHub() ? System.getenv("HOSTNAME") : "localhost";
93 | }
94 | }
--------------------------------------------------------------------------------
/src/test/java/de/f0rce/signaturepad/View.java:
--------------------------------------------------------------------------------
1 | package de.f0rce.signaturepad;
2 |
3 | import com.vaadin.flow.component.button.Button;
4 | import com.vaadin.flow.component.dialog.Dialog;
5 | import com.vaadin.flow.component.html.Div;
6 | import com.vaadin.flow.component.html.Image;
7 | import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
8 | import com.vaadin.flow.router.Route;
9 |
10 | @Route("")
11 | public class View extends Div {
12 |
13 | public View() {
14 | HorizontalLayout vl = new HorizontalLayout();
15 |
16 | SignaturePad signature = new SignaturePad();
17 | signature.setHeight("100px");
18 | signature.setWidth("300px");
19 |
20 | Button button = new Button("Undo");
21 | Button button2 = new Button("Save");
22 |
23 | Dialog dialog = new Dialog();
24 | dialog.setSizeFull();
25 | dialog.setCloseOnOutsideClick(true);
26 | dialog.setCloseOnEsc(true);
27 | dialog.setResizable(true);
28 |
29 | Image sign = new Image();
30 |
31 | vl.add(signature, sign);
32 |
33 | dialog.add(vl, button, button2);
34 |
35 | Button button3 = new Button("Open Sign Dialog");
36 |
37 | this.add(dialog, button3);
38 |
39 | button3.addClickListener(event -> {
40 | dialog.open();
41 | });
42 |
43 | button.addClickListener(event -> {
44 | signature.undo();
45 | });
46 |
47 | button2.addClickListener(event -> {
48 | sign.setSrc(signature.getImageURI());
49 | });
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/java/de/f0rce/signaturepad/ViewIT.java:
--------------------------------------------------------------------------------
1 | package de.f0rce.signaturepad;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | import com.vaadin.testbench.TestBenchElement;
7 |
8 | public class ViewIT extends AbstractViewTest {
9 |
10 | @Test
11 | public void componentWorks() {
12 | final TestBenchElement paperSlider = $("signature_pad").first();
13 | // Check that signature_pad contains at least one other element, which means that
14 | // is has been upgraded to a custom element and not just rendered as an empty
15 | // tag
16 | Assert.assertTrue(
17 | paperSlider.$(TestBenchElement.class).all().size() > 0);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains project specific customizations for the webpack build.
3 | * It is autogenerated if it didn't exist or if it was made for an older
4 | * incompatible version.
5 | *
6 | * Defaults are provided in an autogenerated webpack.generated.js file and used by this file.
7 | * The webpack.generated.js file is overwritten on each build and no customization can be done there.
8 | */
9 | const merge = require('webpack-merge');
10 | const flowDefaults = require('./webpack.generated.js');
11 |
12 | module.exports = merge(flowDefaults, {
13 |
14 | });
15 |
16 | /**
17 | * This file can be used to configure the flow plugin defaults.
18 | *
19 | * // Add a custom plugin
20 | * flowDefaults.plugins.push(new MyPlugin());
21 | *
22 | * // Update the rules to also transpile `.mjs` files
23 | * if (!flowDefaults.module.rules[0].test) {
24 | * throw "Unexpected structure in generated webpack config";
25 | * }
26 | * flowDefaults.module.rules[0].test = /\.m?js$/
27 | *
28 | * // Include a custom JS in the entry point in addition to generated-flow-imports.js
29 | * if (typeof flowDefaults.entry.index != "string") {
30 | * throw "Unexpected structure in generated webpack config";
31 | * }
32 | * flowDefaults.entry.index = [flowDefaults.entry.index, "myCustomFile.js"];
33 | *
34 | * or add new configuration in the merge block.
35 | *
36 | * module.exports = merge(flowDefaults, {
37 | * mode: 'development',
38 | * devtool: 'inline-source-map'
39 | * });
40 | *
41 | */
42 |
--------------------------------------------------------------------------------
/webpack.generated.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NOTICE: this is an auto-generated file
3 | *
4 | * This file has been generated by the `flow:prepare-frontend` maven goal.
5 | * This file will be overwritten on every run. Any custom changes should be made to webpack.config.js
6 | */
7 | const fs = require('fs');
8 | const CopyWebpackPlugin = require('copy-webpack-plugin');
9 | const CompressionPlugin = require('compression-webpack-plugin');
10 | const {BabelMultiTargetPlugin} = require('webpack-babel-multi-target-plugin');
11 | const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin');
12 |
13 | // Flow plugins
14 | const StatsPlugin = require('@vaadin/stats-plugin');
15 | const ThemeLiveReloadPlugin = require('@vaadin/theme-live-reload-plugin');
16 | const { ApplicationThemePlugin, processThemeResources, extractThemeName, findParentThemes } = require('@vaadin/application-theme-plugin');
17 |
18 | const path = require('path');
19 | const baseDir = path.resolve(__dirname);
20 | // the folder of app resources (main.js and flow templates)
21 |
22 | // this matches /themes/my-theme/ and is used to check css url handling and file path build.
23 | const themePartRegex = /(\\|\/)themes\1[\s\S]*?\1/;
24 |
25 | const frontendFolder = require('path').resolve(__dirname, 'frontend');
26 |
27 | const fileNameOfTheFlowGeneratedMainEntryPoint = require('path').resolve(__dirname, 'target/frontend/generated-flow-imports.js');
28 | const mavenOutputFolderForFlowBundledFiles = require('path').resolve(__dirname, 'target/classes/META-INF/VAADIN');
29 |
30 | const devmodeGizmoJS = '@vaadin/flow-frontend/VaadinDevmodeGizmo.js'
31 |
32 | // public path for resources, must match Flow VAADIN_BUILD
33 | const build = 'build';
34 | // public path for resources, must match the request used in flow to get the /build/stats.json file
35 | const config = 'config';
36 | // folder for outputting index.js bundle, etc.
37 | const buildFolder = `${mavenOutputFolderForFlowBundledFiles}/${build}`;
38 | // folder for outputting stats.json
39 | const confFolder = `${mavenOutputFolderForFlowBundledFiles}/${config}`;
40 | // file which is used by flow to read templates for server `@Id` binding
41 | const statsFile = `${confFolder}/stats.json`;
42 |
43 | // Folders in the project which can contain static assets.
44 | const projectStaticAssetsFolders = [
45 | path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources'),
46 | path.resolve(__dirname, 'src', 'main', 'resources', 'static'),
47 | frontendFolder
48 | ];
49 |
50 | const projectStaticAssetsOutputFolder = require('path').resolve(__dirname, 'target/classes/META-INF/VAADIN/static');
51 |
52 | // Folders in the project which can contain application themes
53 | const themeProjectFolders = projectStaticAssetsFolders.map((folder) =>
54 | path.resolve(folder, 'themes')
55 | );
56 |
57 |
58 | // Target flow-fronted auto generated to be the actual target folder
59 | const flowFrontendFolder = require('path').resolve(__dirname, 'target/frontend');
60 |
61 | // make sure that build folder exists before outputting anything
62 | const mkdirp = require('mkdirp');
63 |
64 | const devMode = process.argv.find(v => v.indexOf('webpack-dev-server') >= 0);
65 |
66 | !devMode && mkdirp(buildFolder);
67 | mkdirp(confFolder);
68 |
69 | let stats;
70 |
71 | const transpile = !devMode || process.argv.find(v => v.indexOf('--transpile-es5') >= 0);
72 |
73 | const watchDogPrefix = '--watchDogPort=';
74 | let watchDogPort = devMode && process.argv.find(v => v.indexOf(watchDogPrefix) >= 0);
75 | let client;
76 | if (watchDogPort) {
77 | watchDogPort = watchDogPort.substr(watchDogPrefix.length);
78 | const runWatchDog = () => {
79 | client = new require('net').Socket();
80 | client.setEncoding('utf8');
81 | client.on('error', function () {
82 | console.log("Watchdog connection error. Terminating webpack process...");
83 | client.destroy();
84 | process.exit(0);
85 | });
86 | client.on('close', function () {
87 | client.destroy();
88 | runWatchDog();
89 | });
90 |
91 | client.connect(watchDogPort, 'localhost');
92 | }
93 |
94 | runWatchDog();
95 | }
96 |
97 | const flowFrontendThemesFolder = path.resolve(flowFrontendFolder, 'themes');
98 | const frontendGeneratedFolder = path.resolve(frontendFolder, "generated");
99 | const themeOptions = {
100 | devMode: devMode,
101 | // The following matches ./frontend/generated/theme.js
102 | // and for theme in JAR that is copied to target/frontend/themes/
103 | themeResourceFolder: flowFrontendThemesFolder,
104 | themeProjectFolders: themeProjectFolders,
105 | projectStaticAssetsOutputFolder: projectStaticAssetsOutputFolder,
106 | frontendGeneratedFolder: frontendGeneratedFolder
107 | };
108 | let themeName = undefined;
109 | let themeWatchFolders = undefined;
110 | if (devMode) {
111 | // Current theme name is being extracted from theme.js located in
112 | // frontend/generated folder
113 | themeName = extractThemeName(frontendGeneratedFolder);
114 | const parentThemePaths = findParentThemes(themeName, themeOptions);
115 | const currentThemeFolders = [...projectStaticAssetsFolders
116 | .map((folder) => path.resolve(folder, "themes", themeName)),
117 | path.resolve(flowFrontendThemesFolder, themeName)];
118 | // Watch the components folders for component styles update in both
119 | // current theme and parent themes. Other folders or CSS files except
120 | // 'styles.css' should be referenced from `styles.css` anyway, so no need
121 | // to watch them.
122 | themeWatchFolders = [...currentThemeFolders, ...parentThemePaths]
123 | .map((themeFolder) => path.resolve(themeFolder, "components"));
124 | }
125 |
126 | const processThemeResourcesCallback = (logger) => processThemeResources(themeOptions, logger);
127 |
128 | exports = {
129 | frontendFolder: `${frontendFolder}`,
130 | buildFolder: `${buildFolder}`,
131 | confFolder: `${confFolder}`
132 | };
133 |
134 | module.exports = {
135 | mode: 'production',
136 | context: frontendFolder,
137 | entry: {
138 | bundle: fileNameOfTheFlowGeneratedMainEntryPoint,
139 | ...(devMode && { gizmo: devmodeGizmoJS })
140 | },
141 |
142 | output: {
143 | filename: `${build}/vaadin-[name]-[contenthash].cache.js`,
144 | path: mavenOutputFolderForFlowBundledFiles,
145 | publicPath: 'VAADIN/',
146 | },
147 |
148 | resolve: {
149 | // Search for import 'x/y' inside these folders, used at least for importing an application theme
150 | modules: [
151 | 'node_modules',
152 | flowFrontendFolder,
153 | ...projectStaticAssetsFolders,
154 | ],
155 | extensions: ['.ts', '.js'],
156 | alias: {
157 | Frontend: frontendFolder
158 | }
159 | },
160 |
161 | devServer: {
162 | // webpack-dev-server serves ./ , webpack-generated, and java webapp
163 | contentBase: [mavenOutputFolderForFlowBundledFiles, 'src/main/webapp'],
164 | after: function(app, server) {
165 | app.get(`/stats.json`, function(req, res) {
166 | res.json(stats);
167 | });
168 | app.get(`/stats.hash`, function(req, res) {
169 | res.json(stats.hash.toString());
170 | });
171 | app.get(`/assetsByChunkName`, function(req, res) {
172 | res.json(stats.assetsByChunkName);
173 | });
174 | app.get(`/stop`, function(req, res) {
175 | // eslint-disable-next-line no-console
176 | console.log("Stopped 'webpack-dev-server'");
177 | process.exit(0);
178 | });
179 | }
180 | },
181 |
182 | module: {
183 | rules: [
184 | ...(transpile ? [
185 | {
186 | test: /\.tsx?$/,
187 | use: [ BabelMultiTargetPlugin.loader(), 'ts-loader' ],
188 | }
189 | ] : [{
190 | test: /\.tsx?$/,
191 | use: ['ts-loader']
192 | }]),
193 | ...(transpile ? [{ // Files that Babel has to transpile
194 | test: /\.js$/,
195 | use: [BabelMultiTargetPlugin.loader()]
196 | }] : []),
197 | {
198 | test: /\.css$/i,
199 | use: [
200 | {
201 | loader: 'css-loader',
202 | options: {
203 | url: (url, resourcePath) => {
204 | // css urls may contain query string or fragment identifiers
205 | // that should removed before resolving real path
206 | // e.g
207 | // ../webfonts/fa-solid-900.svg#fontawesome
208 | // ../webfonts/fa-brands-400.eot?#iefix
209 | if(url.includes('?'))
210 | url = url.substring(0, url.indexOf('?'));
211 | if(url.includes('#'))
212 | url = url.substring(0, url.indexOf('#'));
213 |
214 | // Only translate files from node_modules
215 | const resolve = resourcePath.match(/(\\|\/)node_modules\1/)
216 | && fs.existsSync(path.resolve(path.dirname(resourcePath), url));
217 | const themeResource = resourcePath.match(themePartRegex) && url.match(/^themes\/[\s\S]*?\//);
218 | return resolve || themeResource;
219 | },
220 | // use theme-loader to also handle any imports in css files
221 | importLoaders: 1
222 | },
223 | },
224 | {
225 | // theme-loader will change any url starting with './' to start with 'VAADIN/static' instead
226 | // NOTE! this loader should be here so it's run before css-loader as loaders are applied Right-To-Left
227 | loader: '@vaadin/theme-loader',
228 | options: {
229 | devMode: devMode
230 | }
231 | }
232 | ],
233 | },
234 | {
235 | // File-loader only copies files used as imports in .js files or handled by css-loader
236 | test: /\.(png|gif|jpg|jpeg|svg|eot|woff|woff2|otf|ttf)$/,
237 | use: [{
238 | loader: 'file-loader',
239 | options: {
240 | outputPath: 'static/',
241 | name(resourcePath, resourceQuery) {
242 | if (resourcePath.match(/(\\|\/)node_modules\1/)) {
243 | return /(\\|\/)node_modules\1(?!.*node_modules)([\S]+)/.exec(resourcePath)[2].replace(/\\/g, "/");
244 | }
245 | if (resourcePath.match(/(\\|\/)frontend\1/)) {
246 | return /(\\|\/)frontend\1(?!.*frontend)([\S]+)/.exec(resourcePath)[2].replace(/\\/g, "/");
247 | }
248 | return '[path][name].[ext]';
249 | }
250 | }
251 | }],
252 | },
253 | ]
254 | },
255 | performance: {
256 | maxEntrypointSize: 2097152, // 2MB
257 | maxAssetSize: 2097152 // 2MB
258 | },
259 | plugins: [
260 | // Generate compressed bundles when not devMode
261 | ...(devMode ? [] : [new CompressionPlugin()]),
262 |
263 | // Transpile with babel, and produce different bundles per browser
264 | ...(transpile ? [new BabelMultiTargetPlugin({
265 | babel: {
266 | plugins: [
267 | // workaround for Safari 10 scope issue (https://bugs.webkit.org/show_bug.cgi?id=159270)
268 | "@babel/plugin-transform-block-scoping",
269 |
270 | // Edge does not support spread '...' syntax in object literals (#7321)
271 | "@babel/plugin-proposal-object-rest-spread"
272 | ],
273 |
274 | presetOptions: {
275 | useBuiltIns: false // polyfills are provided from webcomponents-loader.js
276 | }
277 | },
278 | targets: {
279 | 'es6': { // Evergreen browsers
280 | browsers: [
281 | // It guarantees that babel outputs pure es6 in bundle and in stats.json
282 | // In the case of browsers no supporting certain feature it will be
283 | // covered by the webcomponents-loader.js
284 | 'last 1 Chrome major versions'
285 | ],
286 | },
287 | 'es5': { // IE11
288 | browsers: [
289 | 'ie 11'
290 | ],
291 | tagAssetsWithKey: true, // append a suffix to the file name
292 | }
293 | }
294 | })] : []),
295 |
296 | new ApplicationThemePlugin(themeOptions),
297 |
298 | ...(devMode && themeName ? [new ExtraWatchWebpackPlugin({
299 | files: [],
300 | dirs: themeWatchFolders
301 | }), new ThemeLiveReloadPlugin(processThemeResourcesCallback)] : []),
302 |
303 | new StatsPlugin({
304 | devMode: devMode,
305 | statsFile: statsFile,
306 | setResults: function (statsFile) {
307 | stats = statsFile;
308 | }
309 | }),
310 |
311 | // Generates the stats file for flow `@Id` binding.
312 | function (compiler) {
313 | compiler.hooks.done.tapAsync('FlowIdPlugin', (compilation, done) => {
314 | // trigger live reload via server
315 | if (client) {
316 | client.write('reload\n');
317 | }
318 | done();
319 | });
320 | },
321 |
322 | // Copy webcomponents polyfills. They are not bundled because they
323 | // have its own loader based on browser quirks.
324 | new CopyWebpackPlugin([{
325 | from: `${baseDir}/node_modules/@webcomponents/webcomponentsjs`,
326 | to: `${build}/webcomponentsjs/`,
327 | ignore: ['*.md', '*.json']
328 | }]),
329 | ]
330 | };
331 |
--------------------------------------------------------------------------------