From 326553915f92c16025c59bfcab92638f80505fdf Mon Sep 17 00:00:00 2001 From: Max Berger Date: Wed, 4 Mar 2026 20:43:49 +0100 Subject: [PATCH 1/5] Added github workflow for integ tests --- .github/workflows/integ_test.yml | 27 +++++++++++++++++++++++++++ .gitignore | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/integ_test.yml diff --git a/.github/workflows/integ_test.yml b/.github/workflows/integ_test.yml new file mode 100644 index 00000000..48d788d6 --- /dev/null +++ b/.github/workflows/integ_test.yml @@ -0,0 +1,27 @@ +name: Integration Tests +on: [push, pull_request] + +jobs: + integ-test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install uv + run: pip install uv + - name: Install Playwright Browsers + run: uv run --extra integ_test playwright install --with-deps + - name: Run Integration Tests + run: uv run --extra integ_test pytest --junitxml=pytest-results.xml integ_tests + - uses: mikepenz/action-junit-report@v6 + if: ${{ failure() && (github.event.pull_request.head.repo.full_name != github.repository) }} + with: + report_paths: 'pytest-results.xml' + annotate_only: true # forked repo cannot write to checks so just do annotations + - uses: mikepenz/action-junit-report@v6 + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + with: + report_paths: 'pytest-results.xml' diff --git a/.gitignore b/.gitignore index 11b196df..cd0b601a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,6 @@ coverage.xml .vscode .sass-cache Gemfile.lock - +pytest-results.xml pytestdebug.log uv.lock From 066a133860354a70d5ca6782f2498558873d1a54 Mon Sep 17 00:00:00 2001 From: Max Berger Date: Wed, 4 Mar 2026 21:34:29 +0100 Subject: [PATCH 2/5] DeleteCollectionScene: Use proper class --- .../internal_data/DeleteCollectionScene.js | 165 +++++++++--------- 1 file changed, 83 insertions(+), 82 deletions(-) diff --git a/radicale/web/internal_data/DeleteCollectionScene.js b/radicale/web/internal_data/DeleteCollectionScene.js index c7cc1c93..f4e4419e 100644 --- a/radicale/web/internal_data/DeleteCollectionScene.js +++ b/radicale/web/internal_data/DeleteCollectionScene.js @@ -25,97 +25,98 @@ import { LoadingScene } from "./LoadingScene.js"; import { delete_collection } from "./api.js"; /** - * @constructor * @implements {Scene} * @param {string} user * @param {string} password * @param {Collection} collection */ -export function DeleteCollectionScene(user, password, collection) { - /** @type {HTMLElement} */ let html_scene = document.getElementById("deletecollectionscene"); - /** @type {HTMLElement} */ let title_form = html_scene.querySelector("[data-name=title]"); - /** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]"); - /** @type {HTMLInputElement} */ let confirmation_txt = html_scene.querySelector("[data-name=confirmationtxt]"); - /** @type {HTMLElement} */ let delete_confirmation_lbl = html_scene.querySelector("[data-name=deleteconfirmationtext]"); - /** @type {HTMLElement} */ let delete_btn = html_scene.querySelector("[data-name=delete]"); - /** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]"); +export class DeleteCollectionScene { + constructor(user, password, collection) { + /** @type {HTMLElement} */ let html_scene = document.getElementById("deletecollectionscene"); + /** @type {HTMLElement} */ let title_form = html_scene.querySelector("[data-name=title]"); + /** @type {HTMLElement} */ let error_form = html_scene.querySelector("[data-name=error]"); + /** @type {HTMLInputElement} */ let confirmation_txt = html_scene.querySelector("[data-name=confirmationtxt]"); + /** @type {HTMLElement} */ let delete_confirmation_lbl = html_scene.querySelector("[data-name=deleteconfirmationtext]"); + /** @type {HTMLElement} */ let delete_btn = html_scene.querySelector("[data-name=delete]"); + /** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]"); - delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT; - confirmation_txt.value = ""; - confirmation_txt.addEventListener("keydown", onkeydown); + delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT; + confirmation_txt.value = ""; + confirmation_txt.addEventListener("keydown", onkeydown); - /** @type {?number} */ let scene_index = null; - /** @type {?XMLHttpRequest} */ let delete_req = null; - let error = ""; + /** @type {?number} */ let scene_index = null; + /** @type {?XMLHttpRequest} */ let delete_req = null; + let error = ""; - function ondelete() { - let confirmation_text_value = confirmation_txt.value; - if(confirmation_text_value != DELETE_CONFIRMATION_TEXT){ - alert("Please type the confirmation text to delete this collection."); - return; + function ondelete() { + let confirmation_text_value = confirmation_txt.value; + if (confirmation_text_value != DELETE_CONFIRMATION_TEXT) { + alert("Please type the confirmation text to delete this collection."); + return; + } + try { + let loading_scene = new LoadingScene(); + push_scene(loading_scene, false); + delete_req = delete_collection(user, password, collection, function (error1) { + if (scene_index === null) { + return; + } + delete_req = null; + if (error1) { + error = error1; + pop_scene(scene_index); + } else { + pop_scene(scene_index - 1); + } + }); + } catch (err) { + console.error(err); + } + return false; } - try { - let loading_scene = new LoadingScene(); - push_scene(loading_scene, false); - delete_req = delete_collection(user, password, collection, function(error1) { - if (scene_index === null) { - return; - } + + function oncancel() { + try { + pop_scene(scene_index - 1); + } catch (err) { + console.error(err); + } + return false; + } + + function onkeydown(event) { + if (event.keyCode !== 13) { + return; + } + ondelete(); + } + + this.show = function () { + this.release(); + scene_index = scene_stack.length - 1; + html_scene.classList.remove("hidden"); + title_form.textContent = collection.displayname || collection.href; + delete_btn.onclick = ondelete; + cancel_btn.onclick = oncancel; + if (error) { + error_form.textContent = "Error: " + error; + error_form.classList.remove("hidden"); + } else { + error_form.classList.add("hidden"); + } + + }; + this.hide = function () { + html_scene.classList.add("hidden"); + cancel_btn.onclick = null; + delete_btn.onclick = null; + }; + this.release = function () { + scene_index = null; + if (delete_req !== null) { + delete_req.abort(); delete_req = null; - if (error1) { - error = error1; - pop_scene(scene_index); - } else { - pop_scene(scene_index - 1); - } - }); - } catch(err) { - console.error(err); - } - return false; + } + }; } - - function oncancel() { - try { - pop_scene(scene_index - 1); - } catch(err) { - console.error(err); - } - return false; - } - - function onkeydown(event){ - if (event.keyCode !== 13) { - return; - } - ondelete(); - } - - this.show = function() { - this.release(); - scene_index = scene_stack.length - 1; - html_scene.classList.remove("hidden"); - title_form.textContent = collection.displayname || collection.href; - delete_btn.onclick = ondelete; - cancel_btn.onclick = oncancel; - if(error){ - error_form.textContent = "Error: " + error; - error_form.classList.remove("hidden"); - }else{ - error_form.classList.add("hidden"); - } - - }; - this.hide = function() { - html_scene.classList.add("hidden"); - cancel_btn.onclick = null; - delete_btn.onclick = null; - }; - this.release = function() { - scene_index = null; - if (delete_req !== null) { - delete_req.abort(); - delete_req = null; - } - }; } \ No newline at end of file From 95938766c4752df3f3c7fee954eae6c054994391 Mon Sep 17 00:00:00 2001 From: Max Berger Date: Wed, 4 Mar 2026 21:35:00 +0100 Subject: [PATCH 3/5] Add confirmation on delete of share-by-token --- integ_tests/test_sharing.py | 2 ++ radicale/web/internal_data/ShareCollectionScene.js | 13 ++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/integ_tests/test_sharing.py b/integ_tests/test_sharing.py index 63d7456f..9c28486c 100644 --- a/integ_tests/test_sharing.py +++ b/integ_tests/test_sharing.py @@ -29,6 +29,7 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non expect( page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden) img[alt='RO']") ).to_be_visible() + page.once("dialog", lambda dialog: dialog.accept()) page.click('tr:not(.hidden) button[data-name="delete"]', strict=True) expect( page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)") @@ -40,6 +41,7 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non expect( page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden) img[alt='RW']") ).to_be_visible() + page.once("dialog", lambda dialog: dialog.accept()) page.click('tr:not(.hidden) button[data-name="delete"]', strict=True) expect( page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)") diff --git a/radicale/web/internal_data/ShareCollectionScene.js b/radicale/web/internal_data/ShareCollectionScene.js index 7ae02a80..4caf4fec 100644 --- a/radicale/web/internal_data/ShareCollectionScene.js +++ b/radicale/web/internal_data/ShareCollectionScene.js @@ -28,14 +28,6 @@ import { Collection } from "./models.js"; import { Scene, pop_scene, scene_stack } from "./scene_manager.js"; /** - * @constructor - * @implements {Scene} - * @param {string} user - * @param {string} password - * @param {Collection} collection The collection on which to edit sharing setting. Must exist. - */ -/** - * @constructor * @implements {Scene} * @param {string} user * @param {string} password @@ -147,10 +139,13 @@ function add_share_rows(user, password, collection, shares) { } /** @type {HTMLElement} */ let delete_btn = node.querySelector("[data-name=delete]"); delete_btn.onclick = function () { + if (!confirm("Are you sure you want to delete share " + pathortoken + "?")) { + return; + } delete_share_by_token( user, password, - share["PathOrToken"], + pathortoken, function () { update_share_list(user, password, collection); }, From 111a544f5733aca69f3138446878c0d7743f49ae Mon Sep 17 00:00:00 2001 From: Max Berger Date: Wed, 4 Mar 2026 21:43:20 +0100 Subject: [PATCH 4/5] Ensure only tokens are shown in tokens list --- radicale/web/internal_data/ShareCollectionScene.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/web/internal_data/ShareCollectionScene.js b/radicale/web/internal_data/ShareCollectionScene.js index 4caf4fec..399009fc 100644 --- a/radicale/web/internal_data/ShareCollectionScene.js +++ b/radicale/web/internal_data/ShareCollectionScene.js @@ -117,10 +117,10 @@ function add_share_rows(user, password, collection, shares) { shares.forEach(function (share) { let pathortoken = share["PathOrToken"] || ""; let pathmapped = share["PathMapped"] || ""; - if ( + if (( collection.href.includes(pathmapped) || collection.href.includes(pathortoken) - ) { + ) && (share["ShareType"] === "token")) { let node = /** @type {HTMLElement} */ (template.cloneNode(true)); node.classList.remove("hidden"); /** @type {HTMLInputElement} */ let pathortoken_form = node.querySelector("[data-name=pathortoken]"); From ad320a2576c5a97fa0000ddd54d4a8712b838cb0 Mon Sep 17 00:00:00 2001 From: Max Berger Date: Thu, 5 Mar 2026 20:18:47 +0100 Subject: [PATCH 5/5] Merge integ_test into test --- .github/workflows/integ_test.yml | 27 --------------------------- .github/workflows/test.yml | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 .github/workflows/integ_test.yml diff --git a/.github/workflows/integ_test.yml b/.github/workflows/integ_test.yml deleted file mode 100644 index 48d788d6..00000000 --- a/.github/workflows/integ_test.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Integration Tests -on: [push, pull_request] - -jobs: - integ-test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Install uv - run: pip install uv - - name: Install Playwright Browsers - run: uv run --extra integ_test playwright install --with-deps - - name: Run Integration Tests - run: uv run --extra integ_test pytest --junitxml=pytest-results.xml integ_tests - - uses: mikepenz/action-junit-report@v6 - if: ${{ failure() && (github.event.pull_request.head.repo.full_name != github.repository) }} - with: - report_paths: 'pytest-results.xml' - annotate_only: true # forked repo cannot write to checks so just do annotations - - uses: mikepenz/action-junit-report@v6 - if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} - with: - report_paths: 'pytest-results.xml' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 343185bb..047e76b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -198,3 +198,28 @@ jobs: run: pip install tox - name: Lint run: tox -c pyproject.toml -e flake8,mypy,isort + + integ-test: + timeout-minutes: 60 + runs-on: ubuntu-latest + needs: test-ubuntu-python-newest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install uv + run: pip install uv + - name: Install Playwright Browsers + run: uv run --extra integ_test playwright install --with-deps + - name: Run Integration Tests + run: uv run --extra integ_test pytest --junitxml=pytest-results.xml integ_tests + - uses: mikepenz/action-junit-report@v6 + if: ${{ failure() && (github.event.pull_request.head.repo.full_name != github.repository) }} + with: + report_paths: 'pytest-results.xml' + annotate_only: true # forked repo cannot write to checks so just do annotations + - uses: mikepenz/action-junit-report@v6 + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + with: + report_paths: 'pytest-results.xml'