Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions integ_tests/test_download.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# This file is part of Radicale - CalDAV and CardDAV server
# Copyright © 2026-2026 Max Berger <max@berger.name>
#
# 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 <http://www.gnu.org/licenses/>.

"""
Integration tests for download page
"""

import pathlib
from typing import Any, Generator

import pytest
from playwright.sync_api import Page

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_download_addressbook(page: Page, radicale_server: str) -> None:
login(page, radicale_server)
page.click('.fabcontainer a[data-name="new"]')

# an address book is created
page.select_option('#createcollectionscene select[data-name="type"]', "ADDRESSBOOK")
page.locator('#createcollectionscene input[data-name="displayname"]').fill("Abname")
page.click('#createcollectionscene button[data-name="submit"]')

# Start waiting for the download
with page.expect_download() as download_info:
# Perform the action that initiates download
page.hover("article:not(.hidden)")
page.click('article:not(.hidden) a[data-name="download"]')

download = download_info.value
assert download.suggested_filename == "Abname.vcf"


def test_download_calendar_uses_displayname_ics(
page: Page, radicale_server: str
) -> None:
login(page, radicale_server)
page.click('.fabcontainer a[data-name="new"]')

# a calendar is created
page.select_option('#createcollectionscene select[data-name="type"]', "CALENDAR")
page.locator('#createcollectionscene input[data-name="displayname"]').fill(
"Calname"
)
page.click('#createcollectionscene button[data-name="submit"]')

# Start waiting for the download
with page.expect_download() as download_info:
# Perform the action that initiates download
page.hover("article:not(.hidden)")
page.click('article:not(.hidden) a[data-name="download"]')

download = download_info.value
assert download.suggested_filename == "Calname.ics"
1 change: 1 addition & 0 deletions radicale/web/internal_data/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ <h1>Sign in</h1>
</section>

<section id="collectionsscene" class="hidden">
<div class="error hidden" data-name="collectionsscene_error"></div>
<div class="fabcontainer">
<a href="" class="green" data-name="new" title="Create a new addressbook or calendar">
<img src="css/icons/new.svg" class="icon" alt="➕">
Expand Down
18 changes: 15 additions & 3 deletions radicale/web/internal_data/js/scenes/CollectionsScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { delete_collection } from "../api/api.js";
import { SERVER } from "../constants.js";
import { Collection, CollectionType } from "../models/collection.js";
import { collectionsCache } from "../utils/collections_cache.js";
import { ErrorHandler } from "../utils/error.js";
import { bytesToHumanReadable } from "../utils/misc.js";
import { CreateEditCollectionScene } from "./CreateEditCollectionScene.js";
import { DeleteConfirmationScene } from "./DeleteConfirmationScene.js";
Expand All @@ -48,8 +49,10 @@ export class CollectionsScene {
/** @type {HTMLElement} */ let new_btn = html_scene.querySelector("[data-name=new]");
/** @type {HTMLElement} */ let upload_btn = html_scene.querySelector("[data-name=upload]");
/** @type {HTMLElement} */ let incomingshares_btn = html_scene.querySelector("[data-name=incomingshares]");
/** @type {HTMLElement} */ let error_div = html_scene.querySelector("[data-name=collectionsscene_error]");

/** @type {Array<HTMLElement>} */ let nodes = [];
let errorHandler = new ErrorHandler(error_div);

function onnew() {
try {
Expand Down Expand Up @@ -126,13 +129,18 @@ export class CollectionsScene {
/**
* @param {any[]} collections
* @param {import("../api/sharing.js").Share[]} shares
* @param {boolean} clear_error
*/
function show_collections(collections, shares) {
function show_collections(collections, shares, clear_error) {
/** @type {HTMLElement} */ let navBar = document.querySelector("#logoutview");
let heightOfNavBar = navBar.offsetHeight + "px";
html_scene.style.marginTop = heightOfNavBar;
html_scene.style.height = "calc(100vh - " + heightOfNavBar + ")";

if (clear_error) {
errorHandler.clearError();
}

// Clear old nodes
nodes.forEach(function (node) {
node.parentNode.removeChild(node);
Expand Down Expand Up @@ -223,14 +231,18 @@ export class CollectionsScene {
});
}

this.errorwrapper = function (/** @type {string} */ error) {
errorHandler.setError(error);
if (onerror) onerror(error);
};

this.show = function () {
html_scene.classList.remove("hidden");
new_btn.onclick = onnew;
upload_btn.onclick = onupload;
incomingshares_btn.onclick = onincomingshares;
collectionsCache.getChildCollections(user, password, principal_collection, onerror, show_collections);
collectionsCache.getServerFeatures(user, password, null, maybe_enable_sharing_options);
collectionsCache.getChildCollections(user, password, principal_collection, this.errorwrapper, show_collections);
collectionsCache.getServerFeatures(user, password, this.errorwrapper, maybe_enable_sharing_options);
};
this.hide = function () {
html_scene.classList.add("hidden");
Expand Down
10 changes: 6 additions & 4 deletions radicale/web/internal_data/js/utils/collections_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { is_current_scene, pop_scene, push_scene } from "../scenes/scene_manager

class CollectionsCache {
constructor() {
this.child_collections = null;
/** @type {?Array<import("../models/collection.js").Collection>} */ this.child_collections = null;
/** @type {?Array<import("../api/sharing.js").Share>} */ this.incoming_shares = null;
this.server_features = null;
/** @type {?XMLHttpRequest} */ this.collections_req = null;
Expand All @@ -52,11 +52,11 @@ class CollectionsCache {
* @param {string} password
* @param {import("../models/collection.js").Collection} principal_collection
* @param {function(string):void} onerror
* @param {function(Array<import("../models/collection.js").Collection>, Array<import("../api/sharing.js").Share>):void} displayData
* @param {function(Array<import("../models/collection.js").Collection>, Array<import("../api/sharing.js").Share>, boolean):void} displayData
*/
getChildCollections(user, password, principal_collection, onerror, displayData) {
if (this.child_collections !== null && this.incoming_shares !== null) {
displayData(this.child_collections, this.incoming_shares);
displayData(this.child_collections, this.incoming_shares, false);
return;
}

Expand All @@ -73,11 +73,13 @@ class CollectionsCache {
}
if (error) {
onerror(error);
this.child_collections = [];
this.incoming_shares = [];
pop_scene();
} else if (collections !== null && shares !== null) {
this.child_collections = collections;
this.incoming_shares = shares;
displayData(this.child_collections, this.incoming_shares);
displayData(this.child_collections, this.incoming_shares, true);
pop_scene();
}
};
Expand Down
Loading