diff --git a/README.md b/README.md index 37fa67d..5d7c032 100644 --- a/README.md +++ b/README.md @@ -112,3 +112,25 @@ History ======= This pastebin has quite a long history which isn't reflected entirely in its repository. + +Testing +======= +There are 2 types of tests available to verify that the application still works correctly after the changes: +* unit test +* browser e2e tests + +Unit tests are run by pytest by default when executing (the browser tests will be skipped) +``` +€ pytest +``` +If you'd like to run browser tests you need to pass the `--browser` option to the pytest command. +``` +€ pytest --browser +``` +The browser tests are executed by [Playwright](https://playwright.dev/python/docs/intro). +The tests by default will be run in the headless mode. +But it's possible to run them in the headed mode as well to be able to observe the execution of the tests. +For that, you just need to pass the `--headed` option to the `pytest --browser` command: +``` +€ pytest --browser --headed +``` \ No newline at end of file diff --git a/test/e2e/conftest.py b/test/e2e/conftest.py new file mode 100644 index 0000000..16a95fd --- /dev/null +++ b/test/e2e/conftest.py @@ -0,0 +1,31 @@ +from typing import Generator +import pytest +import subprocess +import sys +import logging +from playwright.sync_api import Page +from test.e2e.env_config import PORT +from test.e2e.pageobjects.create_paste_page import CreatePastePage + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="session", autouse=True) +def application() -> Generator[None, None, None]: + # Before All + log.info(f"Starting Pinnwand application on port {PORT}") + proc = subprocess.Popen( + [sys.executable, "-m", "pinnwand", "http", "--port", str(PORT)] + ) + yield + # After All + log.info("Terminating Pinnwand application") + proc.terminate() + + +@pytest.fixture +def create_paste_page(page: Page) -> CreatePastePage: + create_paste_page = CreatePastePage(page) + create_paste_page.open() + create_paste_page.should_be_opened() + return create_paste_page diff --git a/test/e2e/env_config.py b/test/e2e/env_config.py index 57d93ca..6d1f364 100644 --- a/test/e2e/env_config.py +++ b/test/e2e/env_config.py @@ -1,6 +1,3 @@ -from typing import Generator -import pytest -import subprocess import sys import logging import socket @@ -8,6 +5,13 @@ log = logging.getLogger(__name__) +def is_headless(): + if "--headed" in sys.argv: + return False + else: + return True + + def select_port(): sock = socket.socket() sock.bind(("", 0)) @@ -16,17 +20,3 @@ def select_port(): PORT = str(select_port()) BASE_URL = f"http://localhost:{PORT}/" - - -@pytest.mark.e2e -@pytest.fixture(scope="session", autouse=True) -def application() -> Generator[None, None, None]: - # Before All - log.info(f"Starting Pinnwand application on port {PORT}") - proc = subprocess.Popen( - [sys.executable, "-m", "pinnwand", "http", "--port", PORT] - ) - yield - # After All - log.info("Terminating Pinnwand application") - proc.terminate() diff --git a/test/e2e/pageobjects/base_page.py b/test/e2e/pageobjects/base_page.py index ce2f44b..02c7cee 100644 --- a/test/e2e/pageobjects/base_page.py +++ b/test/e2e/pageobjects/base_page.py @@ -1,13 +1,29 @@ -from playwright.sync_api import Locator, expect +from playwright.sync_api import Page, Locator, expect +import logging + +log = logging.getLogger(__name__) class BasePage: - def __init__(self, locator: Locator, name) -> None: + def __init__(self, page: Page, locator: Locator, name) -> None: + self.page = page self.page_locator = locator self.page_name = name + def open(self, paste_url): + log.info(f"Opening page at {paste_url}") + self.page.goto(paste_url) + + def current_url(self): + return self.page.url + # Expectations def should_be_opened(self): expect( self.page_locator, f"{self.page_name} was not opened" ).to_be_visible() + + def should_have_title(self, title): + expect( + self.page, f"{self.page_name} had incorrect title" + ).to_have_title(title) diff --git a/test/e2e/pageobjects/create_paste_page.py b/test/e2e/pageobjects/create_paste_page.py index 59ab9ab..961c866 100644 --- a/test/e2e/pageobjects/create_paste_page.py +++ b/test/e2e/pageobjects/create_paste_page.py @@ -2,6 +2,7 @@ from test.e2e.env_config import BASE_URL from test.e2e.pageobjects.base_page import BasePage import logging +import string log = logging.getLogger(__name__) @@ -9,33 +10,46 @@ class CreatePastePage(BasePage): def __init__(self, page: Page) -> None: super().__init__( - page.locator(".file-part textarea"), "Create Paste Page" + page, page.locator(".file-part textarea"), "Create Paste Page" ) self.page = page self.url = BASE_URL self.paste_input = page.locator(".file-part textarea") self.submit_button = page.locator(".paste-submit button[type=submit]") + self.add_another_paste_button = page.locator("button.add") def open(self): log.info(f"Opening Pinnwand at {self.url}") self.page.goto(self.url) - def type_paste(self, text): + def type_paste(self, text, paste_number=0): log.info(f"Typing {text} in Paste Input") - self.paste_input.type(text) + self.paste_input.nth(paste_number).type(text) def click_submit(self): log.info("Clicking Submit Button") self.submit_button.click() + def click_add_another_file_button(self): + log.info("Clicking Add Another Paste Button") + self.add_another_paste_button.click() + + # Step sequences + def paste_random_text(self, paste_number=0): + paste_text = string.ascii_letters + string.digits + self.type_paste(paste_text, paste_number) + self.should_have_value_in_paste_input(paste_text, paste_number) + return paste_text + # Expectations - def should_have_title(self, title): + def should_have_value_in_paste_input(self, value, paste_number=0): expect( - self.page, f"{self.page_name} had incorrect title" - ).to_have_title(title) + self.paste_input.nth(paste_number), + f"Paste Input had incorrect value on {self.page_name}", + ).to_have_value(value) - def should_have_value_in_paste_input(self, value): + def should_have_no_value_in_paste_input(self): expect( self.paste_input, - f"Paste Input had incorrect value on {self.page_name}", - ).to_have_value(value) + f"Paste Input was not empty on {self.page_name}", + ).to_be_empty() diff --git a/test/e2e/pageobjects/error_page.py b/test/e2e/pageobjects/error_page.py new file mode 100644 index 0000000..d44f3cc --- /dev/null +++ b/test/e2e/pageobjects/error_page.py @@ -0,0 +1,22 @@ +from test.e2e.pageobjects.base_page import BasePage +from playwright.sync_api import Page, expect +import logging + + +class ErrorPage(BasePage): + def __init__(self, page: Page) -> None: + super().__init__(page, page.locator("article"), "Error Page") + self.error_code = self.page_locator.locator("h1") + self.error_description = self.page_locator.locator("p") + self.page = page + + # Expectations + def should_have_error_text(self, code, description): + expect( + self.error_code, + f"Error code displayed on {self.page_name} was incorrect", + ).to_have_text(code) + expect( + self.error_description, + f"Error description displayed on {self.page_name} was incorrect", + ).to_have_text(description) diff --git a/test/e2e/pageobjects/view_paste_page.py b/test/e2e/pageobjects/view_paste_page.py index 0236967..3d39317 100644 --- a/test/e2e/pageobjects/view_paste_page.py +++ b/test/e2e/pageobjects/view_paste_page.py @@ -7,12 +7,17 @@ class ViewPastePage(BasePage): def __init__(self, page: Page) -> None: - super().__init__(page.locator(".files"), "View Paste Page") + super().__init__(page, page.locator(".files"), "View Paste Page") self.page = page self.source = page.locator(".source") + def click_remove_now_button(self): + log.info("Clicking Remove Now Button") + self.page.get_by_role("link", name="Remove now").click() + # Expectations - def should_have_pasted_text(self, text): + def should_have_pasted_text(self, text, paste_number=0): expect( - self.source, f"Pasted text was incorrect on {self.page_name}" + self.source.nth(paste_number), + f"Pasted text was incorrect on {self.page_name}", ).to_have_text(text) diff --git a/test/e2e/playwright/browser_manager.py b/test/e2e/playwright/browser_manager.py new file mode 100644 index 0000000..3d92983 --- /dev/null +++ b/test/e2e/playwright/browser_manager.py @@ -0,0 +1,16 @@ +from playwright.sync_api import Page, Playwright +import logging +from test.e2e.env_config import is_headless + +log = logging.getLogger(__name__) + + +class BrowserManager: + def __init__(self, playwright: Playwright) -> None: + self.playwright = playwright + + def create_new_context(self) -> Page: + log.info("creating new browser context") + browser = self.playwright.chromium.launch(headless=is_headless()) + context = browser.new_context() + return context.new_page() diff --git a/test/e2e/testscenarios/test_create_paste.py b/test/e2e/testscenarios/test_create_paste.py index 5deb01e..29002bf 100644 --- a/test/e2e/testscenarios/test_create_paste.py +++ b/test/e2e/testscenarios/test_create_paste.py @@ -1,23 +1,54 @@ -from playwright.sync_api import Page -import string +from playwright.sync_api import Page, Playwright +import pytest from test.e2e.pageobjects.create_paste_page import CreatePastePage from test.e2e.pageobjects.view_paste_page import ViewPastePage -import pytest -from test.e2e.env_config import application +from test.e2e.playwright.browser_manager import BrowserManager +from test.e2e.conftest import create_paste_page @pytest.mark.e2e -def test_create_paste(page: Page): - create_paste_page = CreatePastePage(page) - create_paste_page.open() - create_paste_page.should_be_opened() +def test_create_single_paste( + page: Page, playwright: Playwright, create_paste_page: CreatePastePage +): create_paste_page.should_have_title("Create new paste") - paste_text = string.ascii_letters + string.digits - create_paste_page.type_paste(paste_text) - create_paste_page.should_have_value_in_paste_input(paste_text) + pasted_text = create_paste_page.paste_random_text() + create_paste_page.click_submit() + + view_paste_page = ViewPastePage(page) + view_paste_page.should_be_opened() + view_paste_page.should_have_pasted_text(pasted_text) + + paste_url = view_paste_page.current_url() + reopen_created_paste(playwright, paste_url) + + +@pytest.mark.e2e +def test_create_multi_paste( + page: Page, playwright: Playwright, create_paste_page: CreatePastePage +): + first_pasted_text = create_paste_page.paste_random_text(paste_number=0) + create_paste_page.click_add_another_file_button() + + second_pasted_text = create_paste_page.paste_random_text(paste_number=1) + create_paste_page.click_add_another_file_button() + + third_pasted_text = create_paste_page.paste_random_text(paste_number=2) + create_paste_page.click_submit() view_paste_page = ViewPastePage(page) view_paste_page.should_be_opened() - view_paste_page.should_have_pasted_text(paste_text) + view_paste_page.should_have_pasted_text(first_pasted_text, paste_number=0) + view_paste_page.should_have_pasted_text(second_pasted_text, paste_number=1) + view_paste_page.should_have_pasted_text(third_pasted_text, paste_number=2) + + paste_url = view_paste_page.current_url() + reopen_created_paste(playwright, paste_url) + + +def reopen_created_paste(playwright, paste_url): + new_page = BrowserManager(playwright).create_new_context() + new_view_paste_page = ViewPastePage(new_page) + new_view_paste_page.open(paste_url) + new_view_paste_page.should_be_opened() diff --git a/test/e2e/testscenarios/test_delete_paste.py b/test/e2e/testscenarios/test_delete_paste.py new file mode 100644 index 0000000..7fe343c --- /dev/null +++ b/test/e2e/testscenarios/test_delete_paste.py @@ -0,0 +1,26 @@ +from playwright.sync_api import Page, Playwright +import pytest +from test.e2e.pageobjects.create_paste_page import CreatePastePage +from test.e2e.pageobjects.view_paste_page import ViewPastePage +from test.e2e.pageobjects.error_page import ErrorPage +from test.e2e.conftest import create_paste_page + + +@pytest.mark.e2e +def test_delete_single_paste( + page: Page, playwright: Playwright, create_paste_page: CreatePastePage +): + create_paste_page.paste_random_text() + create_paste_page.click_submit() + + view_paste_page = ViewPastePage(page) + paste_url = view_paste_page.current_url() + view_paste_page.click_remove_now_button() + create_paste_page.should_be_opened() + create_paste_page.should_have_no_value_in_paste_input() + + error_page = ErrorPage(page) + error_page.open(paste_url) + error_page.should_be_opened() + error_page.should_have_title("error") + error_page.should_have_error_text("404", "That page does not exist")