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);
+ }
+
+}