diff --git a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java index 08561bb67f4..5f386262209 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java @@ -1754,6 +1754,13 @@ private static void setupPwa(Document document, PwaRegistry registry) { head.appendElement(META_TAG) .attr("name", "apple-mobile-web-app-capable") .attr(CONTENT_ATTRIBUTE, "yes"); + head.appendElement(META_TAG).attr("name", "mobile-web-app-capable") + .attr(CONTENT_ATTRIBUTE, "yes"); + head.appendElement(META_TAG).attr("name", "apple-touch-fullscreen") + .attr(CONTENT_ATTRIBUTE, "yes"); + head.appendElement(META_TAG) + .attr("name", "apple-mobile-web-app-title") + .attr(CONTENT_ATTRIBUTE, config.getShortName()); // Theme color head.appendElement(META_TAG).attr("name", "theme-color") diff --git a/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java b/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java index a395f81a1b2..cbb354c2d69 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/PwaIcon.java @@ -191,6 +191,15 @@ public String getType() { return attributes.get("type"); } + /** + * Gets the value of the {@literal rel} attribute. + * + * @return value of the {@literal rel} attribute + */ + String getRel() { + return attributes.get("rel"); + } + /** * Gets the icon {@link Domain}. * diff --git a/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java b/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java index aa12d7a70bc..0303cced353 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java @@ -68,8 +68,10 @@ public class PwaRegistry implements Serializable { private static final String META_INF_RESOURCES = "/META-INF/resources"; private static final String HEADLESS_PROPERTY = "java.awt.headless"; private static final String APPLE_STARTUP_IMAGE = "apple-touch-startup-image"; - private static final String APPLE_IMAGE_MEDIA = "(device-width: %dpx) and (device-height: %dpx) " - + "and (-webkit-device-pixel-ratio: %d)"; + private static final String APPLE_IMAGE_MEDIA = "screen and (device-width: %dpx) and (device-height: %dpx)" + + " and (-webkit-device-pixel-ratio: %d) and (orientation: %s)"; + private static final String ORIENTATION_PORTRAIT = "portrait"; + private static final String ORIENTATION_LANDSCAPE = "landscape"; private static final String WORKBOX_CACHE_FORMAT = "{ url: '%s', revision: '%s' }"; private String offlineHtml = ""; @@ -486,25 +488,124 @@ static List getIconTemplates(String baseName) { "apple-touch-icon", "")); // IOS device specific splash screens - // iPhone X (1125px x 2436px) + // iPad Pro 12.9 Portrait: + icons.add(new PwaIcon(2048, 2732, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 1024, 1366, 2, ORIENTATION_PORTRAIT))); + // iPad Pro 12.9 Landscape: + icons.add(new PwaIcon(2732, 2048, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 1024, 1366, 2, ORIENTATION_LANDSCAPE))); + + // iPad Pro 11, 10.5 Portrait: + icons.add(new PwaIcon(1668, 2388, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 834, 1194, 2, ORIENTATION_PORTRAIT))); + // iPad Pro 11, 10.5 Landscape: + icons.add(new PwaIcon(2388, 1668, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 834, 1194, 2, ORIENTATION_LANDSCAPE))); + + // iPad Air 10.5 Portrait: + icons.add(new PwaIcon(1668, 2224, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 834, 1112, 2, ORIENTATION_PORTRAIT))); + // iPad Air 10.5 Landscape: + icons.add(new PwaIcon(2224, 1668, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 834, 1112, 2, ORIENTATION_LANDSCAPE))); + + // iPad 10.2 Portrait: + icons.add(new PwaIcon(1620, 2160, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 768, 1024, 2, ORIENTATION_PORTRAIT))); + // iPad 10.2 Landscape: + icons.add(new PwaIcon(2160, 1620, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 768, 1024, 2, ORIENTATION_LANDSCAPE))); + + // iPad Pro 9.7, iPad Air 9.7, iPad 9.7, iPad mini 7.9 portrait + icons.add(new PwaIcon(1536, 2048, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 768, 1024, 2, ORIENTATION_PORTRAIT))); + // iPad Pro 9.7, iPad Air 9.7, iPad 9.7, iPad mini 7.9 landscape + icons.add(new PwaIcon(2048, 1536, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 768, 1024, 2, ORIENTATION_LANDSCAPE))); + + // iPhone 13 Pro Max, iPhone 12 Pro Max portrait + icons.add(new PwaIcon(1284, 2778, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 428, 926, 3, ORIENTATION_PORTRAIT))); + // iPhone 13 Pro Max, iPhone 12 Pro Max landscape + icons.add(new PwaIcon(2778, 1284, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 428, 926, 3, ORIENTATION_LANDSCAPE))); + + // iPhone 13 Pro, iPhone 13, iPhone 12 Pro, iPhone 12 portrait + icons.add(new PwaIcon(1170, 2532, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 390, 844, 3, ORIENTATION_PORTRAIT))); + // iPhone 13 Pro, iPhone 13, iPhone 12 Pro, iPhone 12 landscape + icons.add(new PwaIcon(2532, 1170, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 390, 844, 3, ORIENTATION_LANDSCAPE))); + + // iPhone 13 Mini, iPhone 12 Mini, iPhone 11 Pro, iPhone XS, iPhone X + // portrait icons.add(new PwaIcon(1125, 2436, baseName, PwaIcon.Domain.HEADER, - false, APPLE_STARTUP_IMAGE, - String.format(APPLE_IMAGE_MEDIA, 375, 812, 3))); - - // iPhone 8, 7, 6s, 6 (750px x 1334px) - icons.add(new PwaIcon(750, 1334, baseName, PwaIcon.Domain.HEADER, false, - APPLE_STARTUP_IMAGE, - String.format(APPLE_IMAGE_MEDIA, 375, 667, 2))); - - // iPhone 8 Plus, 7 Plus, 6s Plus, 6 Plus (1242px x 2208px) + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 375, 812, 3, ORIENTATION_PORTRAIT))); + // iPhone 13 Mini, iPhone 12 Mini, iPhone 11 Pro, iPhone XS, iPhone X + // landscape + icons.add(new PwaIcon(2436, 1125, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 375, 812, 3, ORIENTATION_LANDSCAPE))); + + // iPhone 11 Pro Max, iPhone XS Max portrait + icons.add(new PwaIcon(1242, 2688, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 414, 896, 3, ORIENTATION_PORTRAIT))); + // iPhone 11 Pro Max, iPhone XS Max landscape + icons.add(new PwaIcon(2688, 1242, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 414, 896, 3, ORIENTATION_LANDSCAPE))); + + // iPhone 11, iPhone XR portrait + icons.add(new PwaIcon(828, 1792, baseName, PwaIcon.Domain.HEADER, false, + APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, 414, 896, + 2, ORIENTATION_PORTRAIT))); + // iPhone 11, iPhone XR landscape + icons.add(new PwaIcon(1792, 828, baseName, PwaIcon.Domain.HEADER, false, + APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, 414, 896, + 2, ORIENTATION_LANDSCAPE))); + + // iPhone 8 Plus, 7 Plus, 6s Plus, 6 Plus portrait icons.add(new PwaIcon(1242, 2208, baseName, PwaIcon.Domain.HEADER, - false, APPLE_STARTUP_IMAGE, - String.format(APPLE_IMAGE_MEDIA, 414, 763, 3))); - - // iPhone 5 (640px x 1136px) + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 414, 736, 3, ORIENTATION_PORTRAIT))); + // iPhone 8 Plus, 7 Plus, 6s Plus, 6 Plus landscape + icons.add(new PwaIcon(2208, 1242, baseName, PwaIcon.Domain.HEADER, + false, APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, + 414, 736, 3, ORIENTATION_LANDSCAPE))); + + // iPhone 8, 7, 6s, 6, SE 4.7 portrait + icons.add(new PwaIcon(750, 1334, baseName, PwaIcon.Domain.HEADER, false, + APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, 375, 667, + 2, ORIENTATION_PORTRAIT))); + // iPhone 8, 7, 6s, 6, SE 4.7 landscape + icons.add(new PwaIcon(1334, 750, baseName, PwaIcon.Domain.HEADER, false, + APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, 375, 667, + 2, ORIENTATION_LANDSCAPE))); + + // iPhone 5, SE 4, iPod touch 5th Gen and later portrait icons.add(new PwaIcon(640, 1136, baseName, PwaIcon.Domain.HEADER, false, - APPLE_STARTUP_IMAGE, - String.format(APPLE_IMAGE_MEDIA, 320, 568, 2))); + APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, 320, 568, + 2, ORIENTATION_PORTRAIT))); + // iPhone 5, SE 4, iPod touch 5th Gen and later landscape + icons.add(new PwaIcon(1136, 640, baseName, PwaIcon.Domain.HEADER, false, + APPLE_STARTUP_IMAGE, String.format(APPLE_IMAGE_MEDIA, 320, 568, + 2, ORIENTATION_LANDSCAPE))); return icons; } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java index 7a8ced68364..50682b3c340 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/BootstrapHandlerTest.java @@ -631,28 +631,27 @@ public void page_configurator_append_inline_form_files() Document page = pageBuilder.getBootstrapPage(new BootstrapContext( request, null, session, testUI, this::contextRootRelativePath)); - Elements allElements = page.head().getAllElements(); + String scripts = page.getElementsByTag("script").toString(); // Note element 0 is the full head element. - assertStringEquals( + Assert.assertTrue( "File javascript should have been appended to head element", - "", - allElements.get(allElements.size() - 3).toString()); - assertStringEquals( - "File html should have been appended to head element", - "")); + Assert.assertTrue("File html should have been appended to head element", + scripts.contains("", - allElements.get(allElements.size() - 2).toString()); - assertStringEquals("File css should have been appended to head element", - "", - allElements.get(allElements.size() - 1).toString()); + + " color: rgba(255, 255, 0, 1);\n" + "}")); } @Test // 3036 @@ -982,13 +981,10 @@ public void force_wrapping_of_file() Document page = pageBuilder.getBootstrapPage(new BootstrapContext( request, null, session, testUI, this::contextRootRelativePath)); - Elements allElements = page.head().getAllElements(); - - assertStringEquals( - "File css should have been prepended to body element", - "", - allElements.get(allElements.size() - 1).toString()); + assertTrue("File css should have been prepended to body element", + page.getElementsByTag("style").toString().contains( + "")); } @Test diff --git a/flow-server/src/test/java/com/vaadin/flow/server/HandlerHelperTest.java b/flow-server/src/test/java/com/vaadin/flow/server/HandlerHelperTest.java index 2cba837e3dc..4c16cffbfd6 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/HandlerHelperTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/HandlerHelperTest.java @@ -336,10 +336,32 @@ public void publicResources() { expected.add("/icons/icon-32x32.png"); expected.add("/icons/icon-96x96.png"); expected.add("/icons/icon-180x180.png"); + expected.add("/icons/icon-2048x2732.png"); + expected.add("/icons/icon-2732x2048.png"); + expected.add("/icons/icon-1668x2388.png"); + expected.add("/icons/icon-2388x1668.png"); + expected.add("/icons/icon-1668x2224.png"); + expected.add("/icons/icon-2224x1668.png"); + expected.add("/icons/icon-1620x2160.png"); + expected.add("/icons/icon-2160x1620.png"); + expected.add("/icons/icon-1536x2048.png"); + expected.add("/icons/icon-2048x1536.png"); + expected.add("/icons/icon-1284x2778.png"); + expected.add("/icons/icon-2778x1284.png"); + expected.add("/icons/icon-1170x2532.png"); + expected.add("/icons/icon-2532x1170.png"); expected.add("/icons/icon-1125x2436.png"); - expected.add("/icons/icon-750x1334.png"); + expected.add("/icons/icon-2436x1125.png"); + expected.add("/icons/icon-1242x2688.png"); + expected.add("/icons/icon-2688x1242.png"); + expected.add("/icons/icon-828x1792.png"); + expected.add("/icons/icon-1792x828.png"); expected.add("/icons/icon-1242x2208.png"); + expected.add("/icons/icon-2208x1242.png"); + expected.add("/icons/icon-750x1334.png"); + expected.add("/icons/icon-1334x750.png"); expected.add("/icons/icon-640x1136.png"); + expected.add("/icons/icon-1136x640.png"); expected.add("/themes/**"); Set actual = new HashSet<>(); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java b/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java index 90b46055566..f2b4c7babda 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java @@ -20,14 +20,33 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; @PWA(name = "foo", shortName = "bar") public class PwaRegistryTest { + @PWA(name = "Custom Icon Path", shortName = "CIP", iconPath = "icons/splash/foo.png") + private static class PwaWithCustomIconPath { + } + + private static List splashIconsForAppleDevices; + + @BeforeClass + public static void initPwaWithCustomIconPath() throws IOException { + ServletContext context = Mockito.mock(ServletContext.class); + PwaRegistry registry = new PwaRegistry( + PwaWithCustomIconPath.class.getAnnotation(PWA.class), context); + splashIconsForAppleDevices = registry.getIcons().stream().filter( + icon -> "apple-touch-startup-image".equals(icon.getRel())) + .collect(Collectors.toList()); + } + @Test public void pwaIconIsGeneratedBasedOnClasspathIcon_servletContextHasNoResources() throws IOException { @@ -50,4 +69,197 @@ public void pwaIconIsGeneratedBasedOnClasspathIcon_servletContextHasNoResources( // the default image has 47 on the position 36 Assert.assertEquals(26, stream.toByteArray()[36]); } + + @Test + public void pwaWithCustomBaseIconPath_splashScreenIconForAllSupportedAppleDevicesAndOrientationsAreGenerated() { + Assert.assertEquals(26, splashIconsForAppleDevices.size()); + } + + @Test + public void pwaWithCustomBaseIconPath_splashScreenIconForAppleDevices_areGeneratedBasedOnIconPath() { + boolean customBaseNameUsedInIconGeneration = splashIconsForAppleDevices + .stream().allMatch( + icon -> icon.getHref().startsWith("icons/splash/foo")); + Assert.assertTrue(customBaseNameUsedInIconGeneration); + } + + @Test + public void pwaWithCustomBaseIconPath_splashScreenIconForIPadDevices_includeBothOrientations() { + // iPad Pro 12.9 + Predicate iPadPro129 = icon -> (icon.getWidth() == 2048 + && icon.getHeight() == 2732) + || (icon.getWidth() == 2732 && icon.getHeight() == 2048); + List mediaQueriesForIPadPro129 = splashIconsForAppleDevices + .stream().filter(iPadPro129) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPadPro129.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPadPro129.stream() + .filter(media -> media.contains("landscape")).count()); + + // iPad Pro 11, 10.5 + Predicate iPadPro11And105 = icon -> (icon.getWidth() == 1668 + && icon.getHeight() == 2388) + || (icon.getWidth() == 2388 && icon.getHeight() == 1668); + List mediaQueriesForIPadPro11And105 = splashIconsForAppleDevices + .stream().filter(iPadPro11And105) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPadPro11And105.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPadPro11And105.stream() + .filter(media -> media.contains("landscape")).count()); + + // iPad Air 10.5 + Predicate iPadAir105 = icon -> (icon.getWidth() == 1668 + && icon.getHeight() == 2224) + || (icon.getWidth() == 2224 && icon.getHeight() == 1668); + List mediaQueriesForIPadAir105 = splashIconsForAppleDevices + .stream().filter(iPadAir105) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPadAir105.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPadAir105.stream() + .filter(media -> media.contains("landscape")).count()); + + // iPad 10.2 + Predicate iPad102 = icon -> (icon.getWidth() == 1620 + && icon.getHeight() == 2160) + || (icon.getWidth() == 2160 && icon.getHeight() == 1620); + List mediaQueriesForIPad102 = splashIconsForAppleDevices + .stream().filter(iPad102) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPad102.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPad102.stream() + .filter(media -> media.contains("landscape")).count()); + + // iPad Pro 9.7, iPad Air 9.7, iPad 9.7, iPad mini 7.9 + Predicate iPad97And79 = icon -> (icon.getWidth() == 1536 + && icon.getHeight() == 2048) + || (icon.getWidth() == 2048 && icon.getHeight() == 1536); + List mediaQueriesForIPad97And79 = splashIconsForAppleDevices + .stream().filter(iPad97And79) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPad97And79.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPad97And79.stream() + .filter(media -> media.contains("landscape")).count()); + } + + @Test + public void pwaWithCustomBaseIconPath_splashScreenIconForIPhoneDevices_includeBothOrientations() { + // iPhone 13 Pro Max, iPhone 12 Pro Max + Predicate iPhone13ProMaxAnd12ProMax = icon -> (icon + .getWidth() == 1284 && icon.getHeight() == 2778) + || (icon.getWidth() == 2778 && icon.getHeight() == 1284); + List mediaQueriesForIPhone13ProMaxAnd12ProMax = splashIconsForAppleDevices + .stream().filter(iPhone13ProMaxAnd12ProMax) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPhone13ProMaxAnd12ProMax.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPhone13ProMaxAnd12ProMax.stream() + .filter(media -> media.contains("landscape")).count()); + + // iPhone 13 Pro, iPhone 13, iPhone 12 Pro, iPhone 12 + Predicate iPhone13ProAnd12ProAnd13And12 = icon -> (icon + .getWidth() == 1170 && icon.getHeight() == 2532) + || (icon.getWidth() == 2532 && icon.getHeight() == 1170); + List mediaQueriesForIPhone13ProAnd12ProAnd13And12 = splashIconsForAppleDevices + .stream().filter(iPhone13ProAnd12ProAnd13And12) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPhone13ProAnd12ProAnd13And12 + .stream().filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPhone13ProAnd12ProAnd13And12 + .stream().filter(media -> media.contains("landscape")).count()); + + // iPhone 13 Mini, iPhone 12 Mini, iPhone 11 Pro, iPhone XS, iPhone X + Predicate iPhone13MiniAnd12MiniAnd11ProAndXSAndX = icon -> (icon + .getWidth() == 1125 && icon.getHeight() == 2436) + || (icon.getWidth() == 2436 && icon.getHeight() == 1125); + List mediaQueriesForIPhone13MiniAnd12MiniAnd11ProAndXSAndX = splashIconsForAppleDevices + .stream().filter(iPhone13MiniAnd12MiniAnd11ProAndXSAndX) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, + mediaQueriesForIPhone13MiniAnd12MiniAnd11ProAndXSAndX.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, + mediaQueriesForIPhone13MiniAnd12MiniAnd11ProAndXSAndX.stream() + .filter(media -> media.contains("landscape")).count()); + + // iPhone 11 Pro Max, iPhone XS Max + Predicate iPhone11ProMaxAndXSMax = icon -> (icon + .getWidth() == 1242 && icon.getHeight() == 2688) + || (icon.getWidth() == 2688 && icon.getHeight() == 1242); + List mediaQueriesForIPhone11ProMaxAndXSMax = splashIconsForAppleDevices + .stream().filter(iPhone11ProMaxAndXSMax) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPhone11ProMaxAndXSMax.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPhone11ProMaxAndXSMax.stream() + .filter(media -> media.contains("landscape")).count()); + + // iPhone 11, iPhone XR + Predicate iPhone11AndXR = icon -> (icon.getWidth() == 828 + && icon.getHeight() == 1792) + || (icon.getWidth() == 1792 && icon.getHeight() == 828); + List mediaQueriesForIPhone11AndXR = splashIconsForAppleDevices + .stream().filter(iPhone11AndXR) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPhone11AndXR.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPhone11AndXR.stream() + .filter(media -> media.contains("landscape")).count()); + + // iPhone 8 Plus, 7 Plus, 6s Plus, 6 Plus + Predicate iPhone8PlusAnd7PlusAnd6sPlusAnd6Plus = icon -> (icon + .getWidth() == 1242 && icon.getHeight() == 2208) + || (icon.getWidth() == 2208 && icon.getHeight() == 1242); + List mediaQueriesForIPhone8PlusAnd7PlusAnd6sPlusAnd6Plus = splashIconsForAppleDevices + .stream().filter(iPhone8PlusAnd7PlusAnd6sPlusAnd6Plus) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, + mediaQueriesForIPhone8PlusAnd7PlusAnd6sPlusAnd6Plus.stream() + .filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, + mediaQueriesForIPhone8PlusAnd7PlusAnd6sPlusAnd6Plus.stream() + .filter(media -> media.contains("landscape")).count()); + + // iPhone 8, 7, 6s, 6, SE 4.7 + Predicate iPhone8And7And6sAnd6AndSE47 = icon -> (icon + .getWidth() == 750 && icon.getHeight() == 1334) + || (icon.getWidth() == 1334 && icon.getHeight() == 750); + List mediaQueriesForIPhone8And7And6sAnd6AndSE47 = splashIconsForAppleDevices + .stream().filter(iPhone8And7And6sAnd6AndSE47) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPhone8And7And6sAnd6AndSE47 + .stream().filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPhone8And7And6sAnd6AndSE47 + .stream().filter(media -> media.contains("landscape")).count()); + + // iPhone 5, SE 4, iPod touch 5th Gen and later + Predicate iPhone5AndSE47AndIPod5AndLater = icon -> (icon + .getWidth() == 640 && icon.getHeight() == 1136) + || (icon.getWidth() == 1136 && icon.getHeight() == 640); + List mediaQueriesForIPhone5AndSE47AndIPod5AndLater = splashIconsForAppleDevices + .stream().filter(iPhone5AndSE47AndIPod5AndLater) + .map(icon -> icon.asElement().attr("media")) + .collect(Collectors.toList()); + Assert.assertEquals(1, mediaQueriesForIPhone5AndSE47AndIPod5AndLater + .stream().filter(media -> media.contains("portrait")).count()); + Assert.assertEquals(1, mediaQueriesForIPhone5AndSE47AndIPod5AndLater + .stream().filter(media -> media.contains("landscape")).count()); + } + } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java index 24fe90fcae9..0fc70c0e95c 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java @@ -77,6 +77,7 @@ import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR; import static com.vaadin.flow.server.frontend.FrontendUtils.INDEX_HTML; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -620,17 +621,50 @@ public void should_add_metaAndPwa_Inline_Elements_when_appShellPresent() Document document = Jsoup.parse(indexHtml); Elements elements = document.head().getElementsByTag("meta"); - assertEquals(5, elements.size()); - assertEquals("viewport", elements.get(1).attr("name")); - assertEquals("my-viewport", elements.get(1).attr("content")); - assertEquals("apple-mobile-web-app-capable", - elements.get(2).attr("name")); - assertEquals("yes", elements.get(2).attr("content")); - assertEquals("theme-color", elements.get(3).attr("name")); - assertEquals("#ffffff", elements.get(3).attr("content")); - assertEquals("apple-mobile-web-app-status-bar-style", - elements.get(4).attr("name")); - assertEquals("#ffffff", elements.get(4).attr("content")); + assertEquals(8, elements.size()); + + Optional viewPort = findFirstElementByNameAttrEqualTo(elements, + "viewport"); + assertTrue("'viewport' meta link should exist.", viewPort.isPresent()); + assertEquals("my-viewport", viewPort.get().attr("content")); + + Optional appleMobileWebAppCapable = findFirstElementByNameAttrEqualTo( + elements, "apple-mobile-web-app-capable"); + assertTrue("'apple-mobile-web-app-capable' meta link should exist.", + appleMobileWebAppCapable.isPresent()); + assertEquals("yes", appleMobileWebAppCapable.get().attr("content")); + + Optional themeColor = findFirstElementByNameAttrEqualTo( + elements, "theme-color"); + assertTrue("'theme-color' meta link should exists.", + themeColor.isPresent()); + assertEquals("#ffffff", themeColor.get().attr("content")); + + Optional appleMobileWebAppStatusBar = findFirstElementByNameAttrEqualTo( + elements, "apple-mobile-web-app-status-bar-style"); + assertTrue( + "'apple-mobile-web-app-status-bar-style' meta link should exists.", + appleMobileWebAppStatusBar.isPresent()); + assertEquals("#ffffff", + appleMobileWebAppStatusBar.get().attr("content")); + + Optional mobileWebAppCapableElements = findFirstElementByNameAttrEqualTo( + elements, "mobile-web-app-capable"); + assertTrue("'mobile-web-app-capable' meta link should exists.", + mobileWebAppCapableElements.isPresent()); + assertEquals("yes", mobileWebAppCapableElements.get().attr("content")); + + Optional appleTouchFullScreenElements = findFirstElementByNameAttrEqualTo( + elements, "apple-touch-fullscreen"); + assertTrue("'apple-touch-fullscreen' meta link should exist.", + appleTouchFullScreenElements.isPresent()); + assertEquals("yes", appleTouchFullScreenElements.get().attr("content")); + + Optional appleMobileWebAppTitleElements = findFirstElementByNameAttrEqualTo( + elements, "apple-mobile-web-app-title"); + assertTrue("'apple-mobile-web-app-title' should exist.", + appleMobileWebAppTitleElements.isPresent()); + assertEquals("n", appleMobileWebAppTitleElements.get().attr("content")); Elements headInlineAndStyleElements = document.head() .getElementsByTag("style"); @@ -792,6 +826,13 @@ public void tearDown() throws Exception { mocks.cleanup(); } + private Optional findFirstElementByNameAttrEqualTo( + Elements elements, String name) { + return elements.stream() + .filter(element -> name.equals(element.attr("name"))) + .findFirst(); + } + private VaadinServletRequest createRequestWithDestination(String pathInfo, String fetchDest, String fetchMode) { VaadinServletRequest req = createVaadinRequest(pathInfo); diff --git a/flow-tests/test-pwa/src/test/java/com/vaadin/flow/pwatest/ui/PwaTestIT.java b/flow-tests/test-pwa/src/test/java/com/vaadin/flow/pwatest/ui/PwaTestIT.java index 4ce41c5eddf..efc9af823fc 100644 --- a/flow-tests/test-pwa/src/test/java/com/vaadin/flow/pwatest/ui/PwaTestIT.java +++ b/flow-tests/test-pwa/src/test/java/com/vaadin/flow/pwatest/ui/PwaTestIT.java @@ -81,7 +81,8 @@ public void testPwaResources() throws IOException { By.xpath("//link[@rel='apple-touch-icon'][@sizes][@href]")), 1); checkIcons(head.findElements(By.xpath( - "//link[@rel='apple-touch-startup-image'][@sizes][@href]")), 4); + "//link[@rel='apple-touch-startup-image'][@sizes][@href]")), + 26); // test web manifest List elements = head