diff --git a/e2e/attempt/__init__.py b/e2e/attempt/__init__.py new file mode 100644 index 00000000..e750778c --- /dev/null +++ b/e2e/attempt/__init__.py @@ -0,0 +1,3 @@ +# This file is part of the QuestionPy SDK. (https://questionpy.org) +# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. +# (c) Technische Universität Berlin, innoCampus diff --git a/e2e/attempt/test_choices.py b/e2e/attempt/test_choices.py new file mode 100644 index 00000000..a5d2264a --- /dev/null +++ b/e2e/attempt/test_choices.py @@ -0,0 +1,110 @@ +# This file is part of the QuestionPy SDK. (https://questionpy.org) +# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. +# (c) Technische Universität Berlin, innoCampus + +from typing import Any, cast + +import pytest +from playwright.async_api import Page, expect + +from questionpy import Attempt, form + +FORMULATION = """ +
+
+ + {% for choice in question.options.choices %} +
+ +
+ {% endfor %} +
+
+""" + + +class ChoiceModel(form.FormModel): + id: str = form.generated_id() + label: str = form.text_input("Option", required=True) + is_correct: bool = form.checkbox(right_label="Correct answer") + + +class DynamicChoicesFormModel(form.FormModel): + question_text: str = form.text_input("Question", required=True) + choices: list[ChoiceModel] = form.repeat(ChoiceModel, button_label="Add option", minimum=2) + + +class DynamicChoicesAttempt(Attempt): + def __init__(self, *args: Any): + super().__init__(*args) + + self.placeholders["question"] = self.options.question_text + + def _compute_score(self) -> float: + assert self.response + chosen_ids = {k for k, v in self.response.items() if v == "on"} + correct_ids = {choice.id for choice in self.options.choices if choice.is_correct} + total_correct = len(correct_ids) + num_correct_chosen = len(correct_ids & chosen_ids) + return num_correct_chosen / total_correct if total_correct > 0 else 0.0 + + @property + def formulation(self) -> str: + return self.jinja2.from_string(FORMULATION).render() + + @property + def options(self) -> DynamicChoicesFormModel: + return cast("DynamicChoicesFormModel", self.question.options) + + +@pytest.fixture +def form_options() -> type[form.FormModel]: + return DynamicChoicesFormModel + + +@pytest.fixture +def attempt() -> type[Attempt]: + return DynamicChoicesAttempt + + +async def test_dynamic_choices(page: Page) -> None: + # Create question + await page.get_by_role("button", name="New question", exact=True).click() + await page.get_by_label("Question", exact=True).fill("Best programming languages?") + await page.get_by_label("Option", exact=True).nth(0).fill("Python") + await page.get_by_label("Correct answer", exact=True).nth(0).check() + await page.get_by_label("Option", exact=True).nth(1).fill("Java") + await page.get_by_role("button", name="Add option", exact=True).click() + await page.get_by_label("Option", exact=True).nth(2).fill("Swift") + await page.get_by_role("button", name="Add option", exact=True).click() + await page.get_by_label("Option", exact=True).nth(3).fill("Rust") + await page.get_by_label("Correct answer", exact=True).nth(3).check() + await page.get_by_role("button", name="Create and preview", exact=True).click() + + # 1st Attempt + await page.get_by_role("button", name="New attempt", exact=True).click() + await expect(page.get_by_text("Not yet scored", exact=True)).to_be_visible() + formulation = page.frame_locator("iframe") + await expect(formulation.get_by_text("Best programming languages?")).to_be_visible() + await formulation.get_by_label("Java", exact=True).check() + await page.get_by_role("button", name="Save and submit", exact=True).click() + await expect(page.get_by_text("Score: 0.0", exact=True)).to_be_visible() + + # 2nd Attempt + await page.get_by_role("button", name="Restart", exact=True).click() + await expect(page.get_by_text("Not yet scored", exact=True)).to_be_visible() + await formulation.get_by_label("Python", exact=True).check() + await page.get_by_role("button", name="Save and submit", exact=True).click() + await expect(page.get_by_text("Score: 0.5", exact=True)).to_be_visible() + + # 3rd Attempt + await page.get_by_role("button", name="Restart", exact=True).click() + await expect(page.get_by_text("Not yet scored", exact=True)).to_be_visible() + await formulation.get_by_label("Python", exact=True).check() + await formulation.get_by_label("Rust", exact=True).check() + await page.get_by_role("button", name="Save and submit", exact=True).click() + await expect(page.get_by_text("Score: 1.0", exact=True)).to_be_visible() diff --git a/e2e/conftest.py b/e2e/conftest.py index 80f3ca76..021dbaf3 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -13,9 +13,14 @@ from questionpy.form import FormModel, text_input from questionpy_common.api.qtype import QuestionTypeInterface from questionpy_common.environment import PackageInitFunction +from questionpy_common.manifest import Bcp47LanguageTag from questionpy_sdk.webserver.server import WebServer +from questionpy_server.worker.impl.thread import ThreadWorker from questionpy_server.worker.runtime.package_location import FunctionPackageLocation +# Worker needs to be able to import init function from here +package_init_func: PackageInitFunction + @pytest.fixture def url_path() -> str: @@ -27,10 +32,16 @@ def url(unused_tcp_port: int, url_path: str) -> str: return urljoin(f"http://localhost:{unused_tcp_port}/", url_path) -def default_init(package: Package) -> QuestionTypeInterface: +@pytest.fixture +def form_options() -> type[FormModel]: class PackageForm(FormModel): - text_input: str = text_input("Static text label", required=True) + text_input: str = text_input("Text input", required=True) + + return PackageForm + +@pytest.fixture +def attempt() -> type[Attempt]: class TestAttempt(Attempt): def _compute_score(self) -> float: raise NeedsManualScoringError @@ -41,21 +52,38 @@ def _compute_score(self) -> float: "" ) - class PackageQuestion(Question): - attempt_class = TestAttempt - options: PackageForm - - return QuestionTypeWrapper(PackageQuestion, package) + return TestAttempt @pytest.fixture -def init_func() -> PackageInitFunction: - return default_init +def init_func(form_options: type[FormModel], attempt: type[Attempt]) -> PackageInitFunction: + def _package_init_func(package: Package) -> QuestionTypeInterface: + class PackageQuestion(Question): + attempt_class = attempt + + # Set type annotation at runtime + PackageQuestion.__annotations__["options"] = form_options + + return QuestionTypeWrapper(PackageQuestion, package) + + return _package_init_func @pytest.fixture -def manifest() -> Manifest | None: - return None +def manifest(attempt: type[Attempt]) -> Manifest: + # namespace/short_name need to match attempt module + module_name = attempt.__module__ + namespace, short_name, *_ = module_name.split(".", maxsplit=2) + + return Manifest( + short_name=short_name, + namespace=namespace, + version="0.1.0-test", + api_version="0.1", + author="Jane Doe", + name={Bcp47LanguageTag("en"): "E2E Test Package"}, + languages=[Bcp47LanguageTag("en")], + ) @pytest.fixture @@ -68,7 +96,14 @@ async def page( manifest: Manifest | None, ) -> AsyncIterator[Page]: """Overrides pytest-playwright-asyncio's `page` fixture to include setup/teardown for the SDK web server.""" - pkg_location = FunctionPackageLocation.from_function(init_func, manifest) - async with WebServer(package_location=pkg_location, state_storage_path=tmp_path, port=unused_tcp_port): + global package_init_func # noqa: PLW0603 + package_init_func = init_func + + async with WebServer( + package_location=FunctionPackageLocation(__name__, "package_init_func", manifest), + state_storage_path=tmp_path, + port=unused_tcp_port, + worker_class=ThreadWorker, # SubprocessWorker wouldn't see dynamic package_init_func + ): await page.goto(url) yield page diff --git a/e2e/question/__init__.py b/e2e/question/__init__.py new file mode 100644 index 00000000..e750778c --- /dev/null +++ b/e2e/question/__init__.py @@ -0,0 +1,3 @@ +# This file is part of the QuestionPy SDK. (https://questionpy.org) +# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. +# (c) Technische Universität Berlin, innoCampus diff --git a/e2e/question/test_conditions.py b/e2e/question/test_conditions.py new file mode 100644 index 00000000..fad3d6e9 --- /dev/null +++ b/e2e/question/test_conditions.py @@ -0,0 +1,206 @@ +# This file is part of the QuestionPy SDK. (https://questionpy.org) +# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. +# (c) Technische Universität Berlin, innoCampus + +import pytest +from playwright.async_api import Page, expect + +from questionpy import form + + +class RepeatModel(form.FormModel): + checkbox_a: bool = form.checkbox("Checkbox A", "Disable TextInput A") + text_input_a: str | None = form.text_input("TextInput A", disable_if=form.is_checked("checkbox_a")) + text_input_b: str | None = form.text_input("TextInput B", disable_if=form.is_checked("..[checkbox_3]")) + + +class GroupModel(form.FormModel): + checkbox_b: bool = form.checkbox("Checkbox B", "Disable TextInput 3 (group)") + checkbox_c: bool = form.checkbox("Checkbox C", "Disable TextInput C") + text_input_c: str | None = form.text_input("TextInput C", disable_if=form.is_checked("checkbox_c")) + text_input_d: str | None = form.text_input("TextInput D", disable_if=form.is_checked("..[checkbox_4]")) + + +class SectionModel(form.FormModel): + checkbox_d: bool = form.checkbox("Checkbox D", "Disable TextInput 3 (section)") + checkbox_e: bool = form.checkbox("Checkbox E", "Disable TextInput E") + text_input_e: str | None = form.text_input("TextInput E", disable_if=form.is_checked("checkbox_e")) + text_input_f: str | None = form.text_input("TextInput F", disable_if=form.is_checked("..[checkbox_5]")) + + +class PackageForm(form.FormModel): + checkbox_1: bool = form.checkbox("Checkbox 1", "Disable TextInput 1") + text_input_1: str | None = form.text_input("TextInput 1", disable_if=form.is_checked("checkbox_1")) + + checkbox_2: bool = form.checkbox("Checkbox 2", "Hide TextInput 2") + text_input_2: str | None = form.text_input("TextInput 2", hide_if=form.is_checked("checkbox_2")) + + checkbox_3: bool = form.checkbox("Checkbox 3", "Disable TextInput B") + checkbox_4: bool = form.checkbox("Checkbox 4", "Disable TextInput D") + checkbox_5: bool = form.checkbox("Checkbox 5", "Disable TextInput F") + text_input_3: str | None = form.text_input( + "TextInput 3", disable_if=[form.is_checked("group[checkbox_b]"), form.is_checked("section[checkbox_d]")] + ) + + repeat: list[RepeatModel] = form.repeat(RepeatModel, minimum=2) + checkbox_disable_group: bool = form.checkbox("Checkbox Disable Group", "Disable Group") + checkbox_hide_group: bool = form.checkbox("Checkbox Hide Group", "Hide Group") + group: GroupModel = form.group( + "Group", + GroupModel, + disable_if=form.is_checked("checkbox_disable_group"), + hide_if=form.is_checked("checkbox_hide_group"), + ) + section: SectionModel = form.section("Section", SectionModel) + + checkbox_6: bool = form.checkbox("Checkbox 6", "Enable TextInput 4") + text_input_4: str | None = form.text_input("TextInput 4", disable_if=form.is_not_checked("checkbox_6")) + + text_input_equals: str | None = form.text_input("TextInput (equals)") + text_input_5: str | None = form.text_input( + "TextInput 5", disable_if=form.equals("text_input_equals", value="foo bar") + ) + + text_input_does_not_equal: str | None = form.text_input("TextInput (does_not_equal)") + text_input_6: str | None = form.text_input( + "TextInput 6", disable_if=form.does_not_equal("text_input_does_not_equal", value="foo bar") + ) + + text_input_is_in: str | None = form.text_input("TextInput (is_in)") + text_input_7: str | None = form.text_input( + "TextInput 7", disable_if=form.is_in("text_input_is_in", values=["foo", "bar"]) + ) + + +@pytest.fixture +def form_options() -> type[form.FormModel]: + return PackageForm + + +async def test_conditions_root(page: Page) -> None: + await page.get_by_role("button", name="New question", exact=True).click() + + # Disable within root + await expect(page.get_by_text("TextInput 1", exact=True)).to_be_enabled() + await page.get_by_label("Disable TextInput 1", exact=True).check() + await expect(page.get_by_text("TextInput 1", exact=True)).to_be_disabled() + + # Hide within root + await expect(page.get_by_text("TextInput 2", exact=True)).to_be_visible() + await page.get_by_label("Hide TextInput 2", exact=True).check() + await expect(page.get_by_text("TextInput 2", exact=True)).to_be_hidden() + + +async def test_conditions_repetition(page: Page) -> None: + await page.get_by_role("button", name="New question", exact=True).click() + + # Disable within repetition + await expect(page.get_by_label("TextInput A", exact=True)).to_have_count(2) + await expect(page.get_by_label("TextInput A", exact=True).nth(0)).to_be_enabled() + await expect(page.get_by_label("TextInput A", exact=True).nth(1)).to_be_enabled() + await page.get_by_label("Disable TextInput A", exact=True).nth(0).check() + await expect(page.get_by_label("TextInput A", exact=True).nth(0)).to_be_disabled() + await expect(page.get_by_label("TextInput A", exact=True).nth(1)).to_be_enabled() + await page.get_by_label("Disable TextInput A", exact=True).nth(1).check() + await expect(page.get_by_label("TextInput A", exact=True).nth(0)).to_be_disabled() + await expect(page.get_by_label("TextInput A", exact=True).nth(1)).to_be_disabled() + + # Disable in repetition + await expect(page.get_by_label("TextInput B", exact=True)).to_have_count(2) + await expect(page.get_by_label("TextInput B", exact=True).nth(0)).to_be_enabled() + await expect(page.get_by_label("TextInput B", exact=True).nth(1)).to_be_enabled() + await page.get_by_label("Disable TextInput B", exact=True).check() + await expect(page.get_by_label("TextInput B", exact=True).nth(0)).to_be_disabled() + await expect(page.get_by_label("TextInput B", exact=True).nth(1)).to_be_disabled() + + # Note: Disable from repetition is not supported + + +async def test_conditions_group(page: Page) -> None: + await page.get_by_role("button", name="New question", exact=True).click() + + # Disable within group + await expect(page.get_by_text("TextInput C", exact=True)).to_be_enabled() + await page.get_by_label("Disable TextInput C", exact=True).check() + await expect(page.get_by_text("TextInput C", exact=True)).to_be_disabled() + + # Disable in group + await expect(page.get_by_text("TextInput D", exact=True)).to_be_enabled() + await page.get_by_label("Disable TextInput D", exact=True).check() + await expect(page.get_by_text("TextInput D", exact=True)).to_be_disabled() + + # Disable from group + await expect(page.get_by_text("TextInput 3", exact=True)).to_be_enabled() + await page.get_by_label("Disable TextInput 3 (group)", exact=True).check() + await expect(page.get_by_text("TextInput 3", exact=True)).to_be_disabled() + await page.get_by_label("Disable TextInput 3 (group)", exact=True).uncheck() + + # Disable group + await page.get_by_label("Disable Group", exact=True).check() + await expect(page.get_by_text("Disable TextInput 3 (group)", exact=True)).to_be_disabled() + await expect(page.get_by_text("Disable TextInput C", exact=True)).to_be_disabled() + await expect(page.get_by_text("TextInput C", exact=True)).to_be_disabled() + await expect(page.get_by_text("TextInput D", exact=True)).to_be_disabled() + await page.get_by_label("Disable Group", exact=True).uncheck() + + # Hide group + await expect(page.get_by_text("Group", exact=True)).to_be_visible() + await page.get_by_label("Hide Group", exact=True).check() + await expect(page.get_by_text("Group", exact=True)).to_be_hidden() + await expect(page.get_by_text("Checkbox B", exact=True)).to_be_hidden() + await expect(page.get_by_text("Checkbox C", exact=True)).to_be_hidden() + await expect(page.get_by_text("TextInput C", exact=True)).to_be_hidden() + await expect(page.get_by_text("TextInput D", exact=True)).to_be_hidden() + + +async def test_conditions_section(page: Page) -> None: + await page.get_by_role("button", name="New question", exact=True).click() + + # Disable within section + await expect(page.get_by_text("TextInput E", exact=True)).to_be_enabled() + await page.get_by_label("Disable TextInput E", exact=True).check() + await expect(page.get_by_text("TextInput E", exact=True)).to_be_disabled() + + # Disable in section + await expect(page.get_by_text("TextInput F", exact=True)).to_be_enabled() + await page.get_by_label("Disable TextInput F", exact=True).check() + await expect(page.get_by_text("TextInput F", exact=True)).to_be_disabled() + + # Disable from section + await expect(page.get_by_text("TextInput 3", exact=True)).to_be_enabled() + await page.get_by_label("Disable TextInput 3 (section)", exact=True).check() + await expect(page.get_by_text("TextInput 3", exact=True)).to_be_disabled() + + +async def test_conditions_variants(page: Page) -> None: + await page.get_by_role("button", name="New question", exact=True).click() + + # disable_if + await expect(page.get_by_text("TextInput 4", exact=True)).to_be_disabled() + await page.get_by_label("Enable TextInput 4", exact=True).check() + await expect(page.get_by_text("TextInput 4", exact=True)).to_be_enabled() + + # equals + await expect(page.get_by_text("TextInput 5", exact=True)).to_be_enabled() + await page.get_by_text("TextInput (equals)", exact=True).fill("foo") + await expect(page.get_by_text("TextInput 5", exact=True)).to_be_enabled() + await page.get_by_text("TextInput (equals)", exact=True).fill("foo bar") + await expect(page.get_by_text("TextInput 5", exact=True)).to_be_disabled() + + # does_not_equal + await expect(page.get_by_text("TextInput 6", exact=True)).to_be_disabled() + await page.get_by_text("TextInput (does_not_equal)", exact=True).fill("foo") + await expect(page.get_by_text("TextInput 6", exact=True)).to_be_disabled() + await page.get_by_text("TextInput (does_not_equal)", exact=True).fill("foo bar") + await expect(page.get_by_text("TextInput 6", exact=True)).to_be_enabled() + + # is_in + await expect(page.get_by_text("TextInput 7", exact=True)).to_be_enabled() + await page.get_by_text("TextInput (is_in)", exact=True).fill("foo") + await expect(page.get_by_text("TextInput 7", exact=True)).to_be_disabled() + await page.get_by_text("TextInput (is_in)", exact=True).fill("foo bar") + await expect(page.get_by_text("TextInput 7", exact=True)).to_be_enabled() + await page.get_by_text("TextInput (is_in)", exact=True).fill("bar") + await expect(page.get_by_text("TextInput 7", exact=True)).to_be_disabled() + await page.get_by_text("TextInput (is_in)", exact=True).fill("baz") + await expect(page.get_by_text("TextInput 7", exact=True)).to_be_enabled() diff --git a/e2e/question/test_elements.py b/e2e/question/test_elements.py new file mode 100644 index 00000000..0ed386bd --- /dev/null +++ b/e2e/question/test_elements.py @@ -0,0 +1,164 @@ +# This file is part of the QuestionPy SDK. (https://questionpy.org) +# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. +# (c) Technische Universität Berlin, innoCampus + +import pytest +from playwright.async_api import Page, expect + +from questionpy import form + + +class RadioOptions(form.OptionEnum): + FIRST = form.option("First option (radio)") + SECOND = form.option("Second option (radio)") + + +class SelectOptions(form.OptionEnum): + FIRST = form.option("First option (select)") + SECOND = form.option("Second option (select)") + + +class GroupModel(form.FormModel): + text_input: str | None = form.text_input("TextInput label (group)") + + +class RepeatModel(form.FormModel): + text_input: str | None = form.text_input("TextInput label (repeat)") + + +class SectionModel(form.FormModel): + text_input: str | None = form.text_input("TextInput label (section)") + + +class PackageForm(form.FormModel): + checkbox: bool = form.checkbox("Checkbox label", "Checkbox right label", help="Checkbox help") + file_upload: list[form.OptionsFile] = form.file_upload("FileUpload label", help="FileUpload help") + generated_id: str = form.generated_id() + group: GroupModel = form.group("Group label", GroupModel, help="Group help") + hidden: str = form.hidden("hidden value") + radio_group: RadioOptions | None = form.radio_group("RadioGroup label", RadioOptions, help="RadioGroup help") + repeat: list[RepeatModel] = form.repeat(RepeatModel) + rich_text_editor: form.RichTextEditor = form.rich_text_editor("RichTextEditor label", help="RichTextEditor help") + section: SectionModel = form.section("Section header", SectionModel) + select: SelectOptions | None = form.select("Select label", SelectOptions, help="Select help") + static_text: form.StaticTextElement = form.static_text( + "StaticText label", "StaticText text", help="StaticText help" + ) + text_area: str | None = form.text_area("TextArea label", help="TextArea help") + text_input: str = form.text_input("TextInput label", required=True, help="TextInput help") + + +@pytest.fixture +def form_options() -> type[form.FormModel]: + return PackageForm + + +async def _assert_page_state(page: Page) -> None: + await expect(page.get_by_text("Edit question")).to_be_visible() + await expect(page.get_by_role("button", name="Save", exact=True)).to_be_disabled() + await expect(page.get_by_role("button", name="Save and return", exact=True)).to_be_disabled() + await expect(page.get_by_role("button", name="Preview", exact=True)).to_be_enabled() + await expect(page.get_by_role("button", name="Cancel", exact=True)).to_be_enabled() + + await expect(page.get_by_label("Checkbox right label", exact=True)).to_be_checked() + await expect(page.get_by_text("file.txt", exact=True)).to_be_visible() # FileUpload thumbnail + await expect(page.get_by_label("TextInput label (group)", exact=True)).to_have_value("TextInput (group) filled") + await expect(page.get_by_label("Second option (radio)", exact=True)).to_be_checked() + await expect(page.get_by_label("TextInput label (repeat)", exact=True)).to_have_count(2) + await expect(page.get_by_label("TextInput label (repeat)", exact=True).nth(0)).to_have_value( + "TextInput (repeat) filled 0" + ) + await expect(page.get_by_label("TextInput label (repeat)", exact=True).nth(1)).to_have_value( + "TextInput (repeat) filled 2" + ) + editor_iframe = page.frame_locator(".tox iframe") + await expect(editor_iframe.locator("body")).to_have_text("RichTextEditor filled") + option_text = await page.get_by_label("Select label", exact=True).locator("option:checked").inner_text() + assert option_text == "Second option (select)" + await expect(page.get_by_label("TextArea label", exact=True)).to_have_value("TextArea filled") + await expect(page.get_by_label("TextInput label", exact=True)).to_have_value("TextInput filled") + await expect(page.get_by_label("TextInput label (section)", exact=True)).to_have_value("TextInput (section) filled") + + +async def test_elements(page: Page) -> None: + await page.get_by_role("button", name="New question", exact=True).click() + await expect(page.get_by_text("Create question")).to_be_visible() + await expect(page.get_by_role("button", name="Create", exact=True)).to_be_enabled() + await expect(page.get_by_role("button", name="Create and return", exact=True)).to_be_enabled() + await expect(page.get_by_role("button", name="Create and preview", exact=True)).to_be_enabled() + await expect(page.get_by_role("button", name="Cancel", exact=True)).to_be_enabled() + + # Checkbox + await expect(page.get_by_text("Checkbox label", exact=True)).to_be_visible() + await expect(page.get_by_text("Checkbox help", exact=True)).to_be_visible() + await page.get_by_label("Checkbox right label", exact=True).check() + + # FileUpload + await expect(page.get_by_text("FileUpload label", exact=True)).to_be_visible() + await expect(page.get_by_text("FileUpload help", exact=True)).to_be_visible() + await page.locator("input[type='file']").set_input_files({ + "name": "file.txt", + "mimeType": "text/plain", + "buffer": b"file content", + }) + await expect(page.get_by_text("file.txt", exact=True)).to_be_visible() # thumbnail + + # Group + await expect(page.get_by_text("Group label", exact=True)).to_be_visible() + await expect(page.get_by_text("Group help", exact=True)).to_be_visible() + await expect(page.get_by_text("TextInput label (group)", exact=True)).to_be_visible() + await page.get_by_label("TextInput label (group)", exact=True).fill("TextInput (group) filled") + + # RadioGroup + await expect(page.get_by_text("RadioGroup help", exact=True)).to_be_visible() + await expect(page.get_by_text("RadioGroup label", exact=True)).to_be_visible() + await expect(page.get_by_label("First option (radio)", exact=True)).to_be_visible() + await page.get_by_label("Second option (radio)", exact=True).check() + + # Repeat + await page.get_by_label("TextInput label (repeat)", exact=True).fill("TextInput (repeat) filled 0") + await expect(page.get_by_role("button", name="Remove", exact=True)).to_be_disabled() + await page.get_by_role("button", name="Add repetition", exact=True).click() + await page.get_by_label("TextInput label (repeat)", exact=True).nth(1).fill("TextInput (repeat) filled 1") + await page.get_by_role("button", name="Add repetition", exact=True).click() + await page.get_by_label("TextInput label (repeat)", exact=True).nth(2).fill("TextInput (repeat) filled 2") + await page.get_by_role("button", name="Remove", exact=True).nth(1).click() + await expect(page.get_by_label("TextInput label (repeat)", exact=True)).to_have_count(2) + await expect(page.locator("input[value='TextInput (repeat) filled 0']")).to_have_count(1) + await expect(page.locator("input[value='TextInput (repeat) filled 1']")).to_have_count(0) + await expect(page.locator("input[value='TextInput (repeat) filled 2']")).to_have_count(1) + + # RichTextEditor + await expect(page.get_by_text("RichTextEditor label", exact=True)).to_be_visible() + await expect(page.get_by_text("RichTextEditor help", exact=True)).to_be_visible() + editor_iframe = page.frame_locator(".tox iframe") + await editor_iframe.locator("body").fill("RichTextEditor filled") + + # Select + await expect(page.get_by_text("Select help", exact=True)).to_be_visible() + await page.get_by_label("Select label", exact=True).select_option("Second option (select)") + + # StaticText + await expect(page.get_by_text("StaticText label", exact=True)).to_be_visible() + await expect(page.get_by_text("StaticText text", exact=True)).to_be_visible() + await expect(page.get_by_text("StaticText help", exact=True)).to_be_visible() + + # TextArea + await page.get_by_label("TextArea label", exact=True).fill("TextArea filled") + await expect(page.get_by_text("TextArea help", exact=True)).to_be_visible() + + # TextInput + await page.get_by_label("TextInput label", exact=True).fill("TextInput filled") + await expect(page.get_by_text("TextInput help", exact=True)).to_be_visible() + + # Section + await expect(page.get_by_text("Section header", exact=True)).to_be_visible() + await page.get_by_label("TextInput label (section)", exact=True).fill("TextInput (section) filled") + + # Create question and assert data + await page.get_by_role("button", name="Create", exact=True).click() + await _assert_page_state(page) + + # Assert state is restored after page reload + await page.reload() + await _assert_page_state(page) diff --git a/e2e/test_smoke.py b/e2e/test_smoke.py index be097fdb..e1489181 100644 --- a/e2e/test_smoke.py +++ b/e2e/test_smoke.py @@ -5,12 +5,13 @@ async def test_package(page: Page) -> None: await expect(page).to_have_title(re.compile(r"QuestionPy SDK")) - await expect(page.get_by_text("e2e_conftest/default_init")).to_be_visible() - await expect(page.get_by_text("0.1.0-debug")).to_be_visible() - await expect(page.get_by_text("Debug Modulovitch")).to_be_visible() + await expect(page.get_by_text("e2e/conftest")).to_be_visible() + await expect(page.get_by_text("E2E Test Package")).to_be_visible() + await expect(page.get_by_text("0.1.0-test")).to_be_visible() + await expect(page.get_by_text("Jane Doe")).to_be_visible() await page.get_by_role("button", name="New question").click() - await page.get_by_role("textbox", name="Static text label").fill("foo bar") + await page.get_by_role("textbox", name="Text input").fill("foo bar") await page.get_by_role("button", name="Create and preview").click() await expect(page).to_have_title(re.compile(r"Question Preview")) diff --git a/pyproject.toml b/pyproject.toml index 42cfd4c2..3b6d2f7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ extend-ignore-names = ["mcs", "test_*"] "**/questionpy_sdk/webserver/controllers/attempt/errors.py" = ["RUF027"] # unused-async (aiohttp handlers must be async even if they don't use it) "**/questionpy_sdk/webserver/routes/*" = ["RUF029"] +"**/e2e/**/*" = ["PLR09", "PLR2004", "S"] [tool.ruff.lint.pylint] allow-dunder-method-names = ["__get_pydantic_core_schema__"]