diff --git a/integ_tests/test_delete.py b/integ_tests/test_delete.py new file mode 100644 index 00000000..3fe63a5f --- /dev/null +++ b/integ_tests/test_delete.py @@ -0,0 +1,75 @@ +# This file is part of Radicale - CalDAV and CardDAV server +# Copyright © 2026-2026 Max Berger +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +""" +Integration tests for delete collection scene +""" + +import pathlib +from typing import Any, Generator + +import pytest +from playwright.sync_api import Page, expect + +from integ_tests.common import create_collection, login, start_radicale_server + + +@pytest.fixture +def radicale_server(tmp_path: pathlib.Path) -> Generator[str, Any, None]: + yield from start_radicale_server(tmp_path) + + +def test_delete_wrong_confirmation(page: Page, radicale_server: str) -> None: + login(page, radicale_server) + create_collection(page, radicale_server) + + # Open delete scene + page.hover("article:not(.hidden)") + page.click('article:not(.hidden) a[data-name="delete"]', force=True) + + # Input wrong confirmation + page.fill('#deletecollectionscene input[data-name="confirmationtxt"]', "foo") + page.click('#deletecollectionscene button[data-name="delete"]') + + # Check for error message + error_locator = page.locator('#deletecollectionscene span[data-name="error"]') + expect(error_locator).to_be_visible() + expect(error_locator).to_contain_text("Please type DELETE in the confirmation field") + + # Scene should still be visible + expect(page.locator('#deletecollectionscene')).to_be_visible() + + +def test_delete_correct_confirmation(page: Page, radicale_server: str) -> None: + login(page, radicale_server) + create_collection(page, radicale_server) + + # Verify collection exists + expect(page.locator('article:not(.hidden)')).to_have_count(1) + + # Open delete scene + page.hover("article:not(.hidden)") + page.click('article:not(.hidden) a[data-name="delete"]', force=True) + + # Input correct confirmation + page.fill('#deletecollectionscene input[data-name="confirmationtxt"]', "DELETE") + page.click('#deletecollectionscene button[data-name="delete"]') + + # Verify collection is gone + expect(page.locator('article:not(.hidden)')).to_have_count(0) + + # Scene should be hidden + expect(page.locator('#deletecollectionscene')).to_be_hidden() diff --git a/integ_tests/test_edit.py b/integ_tests/test_edit.py new file mode 100644 index 00000000..60ca1b95 --- /dev/null +++ b/integ_tests/test_edit.py @@ -0,0 +1,78 @@ +# This file is part of Radicale - CalDAV and CardDAV server +# Copyright © 2026-2026 Max Berger +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +""" +Integration tests for edit collection scene +""" + +import pathlib +from typing import Any, Generator + +import pytest +from playwright.sync_api import Page, expect + +from integ_tests.common import create_collection, login, start_radicale_server + + +@pytest.fixture +def radicale_server(tmp_path: pathlib.Path) -> Generator[str, Any, None]: + yield from start_radicale_server(tmp_path) + + +def test_edit_save(page: Page, radicale_server: str) -> None: + login(page, radicale_server) + create_collection(page, radicale_server) + + # Get original values + article = page.locator('article:not(.hidden)') + + # Open edit scene + page.hover("article:not(.hidden)") + page.click('article:not(.hidden) a[data-name="edit"]', force=True) + + # Update title and description + new_title = "Updated Title" + new_description = "Updated Description" + page.fill('#editcollectionscene input[data-name="displayname"]', new_title) + page.fill('#editcollectionscene input[data-name="description"]', new_description) + page.click('#editcollectionscene button[data-name="submit"]') + + # Verify updates in the list + expect(article.locator('[data-name="title"]')).to_have_text(new_title) + expect(article.locator('[data-name="description"]')).to_have_text(new_description) + + +def test_edit_cancel(page: Page, radicale_server: str) -> None: + login(page, radicale_server) + create_collection(page, radicale_server) + + # Get original values + article = page.locator('article:not(.hidden)') + original_title = article.locator('[data-name="title"]').text_content() + original_description = article.locator('[data-name="description"]').text_content() + + # Open edit scene + page.hover("article:not(.hidden)") + page.click('article:not(.hidden) a[data-name="edit"]', force=True) + + # Update title and description but cancel + page.fill('#editcollectionscene input[data-name="displayname"]', "Changed Title") + page.fill('#editcollectionscene input[data-name="description"]', "Changed Description") + page.click('#editcollectionscene button[data-name="cancel"]') + + # Verify values remain unchanged + expect(article.locator('[data-name="title"]')).to_have_text(original_title) + expect(article.locator('[data-name="description"]')).to_have_text(original_description) diff --git a/integ_tests/test_upload.py b/integ_tests/test_upload.py new file mode 100644 index 00000000..b852ce37 --- /dev/null +++ b/integ_tests/test_upload.py @@ -0,0 +1,102 @@ +# This file is part of Radicale - CalDAV and CardDAV server +# Copyright © 2026-2026 Max Berger +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +""" +Integration tests for upload page +""" + +import pathlib +import re +from typing import Any, Generator + +import pytest +from playwright.sync_api import Page, expect + +from integ_tests.common import login, start_radicale_server + + +@pytest.fixture +def radicale_server(tmp_path: pathlib.Path) -> Generator[str, Any, None]: + yield from start_radicale_server(tmp_path) + + +def test_upload_zero_files(page: Page, radicale_server: str) -> None: + login(page, radicale_server) + page.click('.fabcontainer a[data-name="upload"]') + + # Click upload without selecting files + page.click('#uploadcollectionscene button[data-name="submit"]') + + # Check for error message at the bottom of the scene + error_locator = page.locator('#uploadcollectionscene > span[data-name="error"]') + expect(error_locator).to_be_visible() + expect(error_locator).to_contain_text("Please select at least one file") + + +def test_upload_one_file_custom_href(page: Page, radicale_server: str, tmp_path: pathlib.Path) -> None: + login(page, radicale_server) + + # Create a fake file to upload + test_file = tmp_path / "test.ics" + test_file.write_text("BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR") + + page.click('.fabcontainer a[data-name="upload"]') + + # Upload 1 file and set custom href + page.set_input_files('#uploadcollectionscene input[data-name="uploadfile"]', str(test_file)) + page.fill('#uploadcollectionscene input[data-name="href"]', "testcollection") + page.click('#uploadcollectionscene button[data-name="submit"]') + + # Wait for upload to complete in the list item + expect(page.locator('#uploadcollectionscene li:not(.hidden) [data-name="success"]')).to_be_visible() + + # Close scene + page.click('#uploadcollectionscene button[data-name="close"]') + + # Verify 1 collection exists with "testcollection" in url + expect(page.locator('article:not(.hidden)')).to_have_count(1) + expect(page.locator('article:not(.hidden) input[data-name="url"]')).to_have_value(re.compile(r".*testcollection/.*")) + + +def test_upload_two_files(page: Page, radicale_server: str, tmp_path: pathlib.Path) -> None: + login(page, radicale_server) + + # Create two fake files + file1 = tmp_path / "test1.ics" + file1.write_text("BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR") + file2 = tmp_path / "test2.ics" + file2.write_text("BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR") + + page.click('.fabcontainer a[data-name="upload"]') + + # Upload 2 files + page.set_input_files('#uploadcollectionscene input[data-name="uploadfile"]', [str(file1), str(file2)]) + + # HREF field should be hidden + expect(page.locator('#uploadcollectionscene input[data-name="href"]')).to_be_hidden() + expect(page.locator('#uploadcollectionscene [data-name="hreflimitmsg"]')).to_be_visible() + + page.click('#uploadcollectionscene button[data-name="submit"]') + + # Wait for uploads to complete + # Wait until 2 entries in the upload list show success + expect(page.locator('#uploadcollectionscene li:not(.hidden) [data-name="success"]')).to_have_count(2) + + # Close scene + page.click('#uploadcollectionscene button[data-name="close"]') + + # Verify 2 collections exist + expect(page.locator('article:not(.hidden)')).to_have_count(2) diff --git a/radicale/web/internal_data/js/scenes/UploadCollectionScene.js b/radicale/web/internal_data/js/scenes/UploadCollectionScene.js index 6aaff39f..9ff5a9fa 100644 --- a/radicale/web/internal_data/js/scenes/UploadCollectionScene.js +++ b/radicale/web/internal_data/js/scenes/UploadCollectionScene.js @@ -46,7 +46,7 @@ export class UploadCollectionScene { /** @type {HTMLElement} */ let href_label = html_scene.querySelector("label[for=href]"); /** @type {HTMLElement} */ let hreflimitmsg_html = html_scene.querySelector("[data-name=hreflimitmsg]"); /** @type {HTMLElement} */ let pending_html = html_scene.querySelector("[data-name=pending]"); - /** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]"); + /** @type {HTMLElement} */ let error_form = html_scene.querySelector(":scope > span[data-name=error]"); let files = uploadfile_form.files; href_form.addEventListener("input", onCleanHREFinput); @@ -177,7 +177,11 @@ export class UploadCollectionScene { hreflimitmsg_html.classList.add("hidden"); href_form.classList.remove("hidden"); href_label.classList.remove("hidden"); - href_form.value = files[0].name.replace(/\.(ics|vcf)$/, ''); + if (files && files.length > 0) { + href_form.value = files[0].name.replace(/\.(ics|vcf)$/, ''); + } else { + href_form.value = ""; + } } return false; }