diff --git a/commafeed-client/src/pages/app/Layout.tsx b/commafeed-client/src/pages/app/Layout.tsx index ed59658d..b2df4f1d 100644 --- a/commafeed-client/src/pages/app/Layout.tsx +++ b/commafeed-client/src/pages/app/Layout.tsx @@ -104,6 +104,12 @@ export default function Layout({ sidebar, header }: LayoutProps) { /> ) + const addButton = ( + dispatch(redirectToAdd())} aria-label="Subscribe"> + + + ) + if (loading) return return ( - - dispatch(redirectToAdd())}> - - - + {addButton} )} {!mobileMenuOpen && ( @@ -151,11 +153,7 @@ export default function Layout({ sidebar, header }: LayoutProps) { - - dispatch(redirectToAdd())}> - - - + {addButton} {header} diff --git a/commafeed-server/playwright_code_generator.sh b/commafeed-server/playwright_code_generator.sh new file mode 100644 index 00000000..f10da601 --- /dev/null +++ b/commafeed-server/playwright_code_generator.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +mvn exec:java -e -Dexec.classpathScope=test -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="codegen localhost:8082" \ No newline at end of file diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index 1035b895..5d288714 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -490,5 +490,12 @@ awaitility test + + com.microsoft.playwright + playwright + 1.24.1 + test + + \ No newline at end of file diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java new file mode 100644 index 00000000..bad04e5c --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/e2e/AuthentificationIT.java @@ -0,0 +1,63 @@ +package com.commafeed.e2e; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.commafeed.CommaFeedApplication; +import com.commafeed.CommaFeedConfiguration; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.assertions.PlaywrightAssertions; + +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; + +@ExtendWith(DropwizardExtensionsSupport.class) +class AuthentificationIT extends PlaywrightTestBase { + + private static final DropwizardAppExtension EXT = new DropwizardAppExtension( + CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml")); + + @Test + void loginFail() { + page.navigate("http://localhost:" + EXT.getLocalPort()); + page.locator("[placeholder='User Name or E-mail']").fill("admin"); + page.locator("[placeholder='Password']").fill("wrong_password"); + page.locator("button:has-text('Log in')").click(); + PlaywrightAssertions.assertThat(page.locator("div[role='alert']")).containsText("wrong username or password"); + } + + @Test + void loginSuccess() { + page.navigate("http://localhost:" + EXT.getLocalPort()); + PlaywrightTestUtils.login(page); + PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all"); + } + + @Test + void registerFailPasswordTooSimple() { + page.navigate("http://localhost:" + EXT.getLocalPort()); + page.locator("text=Sign up!").click(); + page.locator("[placeholder='User Name']").fill("user"); + page.locator("[placeholder='E-mail address']").fill("user@domain.com"); + page.locator("[placeholder='Password']").fill("pass"); + page.locator("button:has-text('Sign up')").click(); + + Locator alert = page.locator("div[role='alert']"); + PlaywrightAssertions.assertThat(alert).containsText("Password must be 8 or more characters in length."); + PlaywrightAssertions.assertThat(alert).containsText("Password must contain 1 or more uppercase characters."); + PlaywrightAssertions.assertThat(alert).containsText("Password must contain 1 or more digit characters."); + PlaywrightAssertions.assertThat(alert).containsText("Password must contain 1 or more special characters."); + } + + @Test + void registerSuccess() { + page.navigate("http://localhost:" + EXT.getLocalPort()); + page.locator("text=Sign up!").click(); + page.locator("[placeholder='User Name']").fill("user"); + page.locator("[placeholder='E-mail address']").fill("user@domain.com"); + page.locator("[placeholder='Password']").fill("MyPassword1!"); + page.locator("button:has-text('Sign up')").click(); + PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all"); + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java b/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java new file mode 100644 index 00000000..3945d2ee --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestBase.java @@ -0,0 +1,139 @@ +package com.commafeed.e2e; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Optional; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestWatcher; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.Browser.NewContextOptions; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.Tracing; + +/** + * Base class for all Playwright tests. + * + *
    + *
  • Takes a screenshot on failure
  • + *
  • Keeps the video on failure
  • + *
  • Saves a trace file on failure
  • + *
+ * + * inspired by https://github.com/microsoft/playwright-java/issues/503#issuecomment-872636373 + * + */ +@ExtendWith(PlaywrightTestBase.SaveArtifactsOnTestFailed.class) +public class PlaywrightTestBase { + + private static Playwright playwright; + private static Browser browser; + + protected Page page; + private BrowserContext context; + + @BeforeAll + static void initBrowser() { + playwright = Playwright.create(); + browser = playwright.chromium().launch(); + } + + @AfterAll + static void closeBrowser() { + playwright.close(); + } + + protected void customizeNewContextOptions(NewContextOptions options) { + } + + protected static class SaveArtifactsOnTestFailed implements TestWatcher, BeforeEachCallback { + + // defined in the config of maven-failsafe-plugin in pom.xml + private final String buildDirectory = System.getProperty("buildDirectory", "target"); + private final String directory = buildDirectory + "/playwright-artifacts"; + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + PlaywrightTestBase testInstance = getTestInstance(context); + + NewContextOptions newContextOptions = new Browser.NewContextOptions().setRecordVideoDir(Paths.get(directory)); + testInstance.customizeNewContextOptions(newContextOptions); + testInstance.context = PlaywrightTestBase.browser.newContext(newContextOptions); + testInstance.context.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true)); + + testInstance.page = testInstance.context.newPage(); + } + + @Override + public void testFailed(ExtensionContext context, Throwable cause) { + PlaywrightTestBase testInstance = getTestInstance(context); + + String fileName = getFileName(context); + + saveScreenshot(testInstance, fileName); + saveTrace(testInstance, fileName); + + testInstance.context.close(); + + saveVideo(testInstance, fileName); + } + + @Override + public void testAborted(ExtensionContext context, Throwable cause) { + PlaywrightTestBase testInstance = getTestInstance(context); + testInstance.context.close(); + testInstance.page.video().delete(); + } + + @Override + public void testDisabled(ExtensionContext context, Optional reason) { + PlaywrightTestBase testInstance = getTestInstance(context); + testInstance.context.close(); + testInstance.page.video().delete(); + } + + @Override + public void testSuccessful(ExtensionContext context) { + PlaywrightTestBase testInstance = getTestInstance(context); + testInstance.context.close(); + testInstance.page.video().delete(); + } + + private PlaywrightTestBase getTestInstance(ExtensionContext context) { + return (PlaywrightTestBase) context.getRequiredTestInstance(); + } + + private String getFileName(ExtensionContext context) { + return String.format("%s.%s-%s", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(), + new SimpleDateFormat("yyyy-MM-dd--HH-mm-ss").format(new Date())); + } + + private void saveScreenshot(PlaywrightTestBase testInstance, String fileName) { + byte[] screenshot = testInstance.page.screenshot(); + try { + Files.write(Paths.get(directory, fileName + ".png"), screenshot); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void saveTrace(PlaywrightTestBase testInstance, String fileName) { + testInstance.context.tracing().stop(new Tracing.StopOptions().setPath(Paths.get(directory, fileName + ".zip"))); + } + + private void saveVideo(PlaywrightTestBase testInstance, String fileName) { + testInstance.page.video().saveAs(Paths.get(directory, fileName + ".webm")); + testInstance.page.video().delete(); + } + } +} diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestUtils.java b/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestUtils.java new file mode 100644 index 00000000..fc3d7705 --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/e2e/PlaywrightTestUtils.java @@ -0,0 +1,16 @@ +package com.commafeed.e2e; + +import com.microsoft.playwright.Page; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class PlaywrightTestUtils { + + public static void login(Page page) { + page.locator("[placeholder='User Name or E-mail']").fill("admin"); + page.locator("[placeholder='Password']").fill("admin"); + page.locator("button:has-text('Log in')").click(); + } + +} diff --git a/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java b/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java new file mode 100644 index 00000000..82051d08 --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/e2e/ReadingIT.java @@ -0,0 +1,78 @@ +package com.commafeed.e2e; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockserver.client.MockServerClient; +import org.mockserver.junit.jupiter.MockServerExtension; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; + +import com.commafeed.CommaFeedApplication; +import com.commafeed.CommaFeedConfiguration; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Locator.WaitForOptions; +import com.microsoft.playwright.assertions.PlaywrightAssertions; + +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; + +@ExtendWith(DropwizardExtensionsSupport.class) +@ExtendWith(MockServerExtension.class) +class ReadingIT extends PlaywrightTestBase { + + private static final DropwizardAppExtension EXT = new DropwizardAppExtension( + CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml")); + + private MockServerClient mockServerClient; + + @BeforeEach + void init(MockServerClient mockServerClient) throws IOException { + this.mockServerClient = mockServerClient; + this.mockServerClient.when(HttpRequest.request().withMethod("GET")) + .respond(HttpResponse.response() + .withBody(IOUtils.toString(getClass().getResource("/feed/rss.xml"), StandardCharsets.UTF_8))); + } + + @Test + void scenario() { + // login + page.navigate("http://localhost:" + EXT.getLocalPort()); + PlaywrightTestUtils.login(page); + PlaywrightAssertions.assertThat(page.locator("text=You don't have any subscriptions yet.")).hasCount(1); + + // subscribe + page.locator("[aria-label='Subscribe']").click(); + page.locator("text=Feed URL *").fill("http://localhost:" + this.mockServerClient.getPort()); + page.locator("button:has-text('Next')").click(); + page.locator("button:has-text('Subscribe')").nth(2).click(); + + // subscription has two unread entries + Locator treeNode = page.locator("nav >> text=CommaFeed test feed2"); + treeNode.waitFor(new WaitForOptions().setTimeout(30000)); + PlaywrightAssertions.assertThat(treeNode).hasCount(1); + + // click on subscription + treeNode.click(); + Locator entries = page.locator("main >> .mantine-Paper-root"); + PlaywrightAssertions.assertThat(entries).hasCount(2); + + // click on first entry + page.locator("text='Item 1'").click(); + PlaywrightAssertions.assertThat(page.locator("text=Item 1 description")).hasCount(1); + PlaywrightAssertions.assertThat(page.locator("text=Item 2 description")).hasCount(0); + // only one unread entry now + PlaywrightAssertions.assertThat(page.locator("nav >> text=CommaFeed test feed1")).hasCount(1); + + // click on second entry + page.locator("text=Item 2").click(); + PlaywrightAssertions.assertThat(page.locator("text=Item 1 description")).hasCount(0); + PlaywrightAssertions.assertThat(page.locator("text=Item 2 description")).hasCount(1); + } + +}