diff --git a/package-lock.json b/package-lock.json index e3feab9d..3a5d08cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.3.3", "svelte": "^5.16.0", + "svelte-autosize": "^1.1.5", "svelte-check": "^4.1.4", "tailwind-merge": "^2.6.0", "tailwind-variants": "^0.3.0", @@ -1805,6 +1806,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/autosize": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/autosize/-/autosize-4.0.3.tgz", + "integrity": "sha512-o0ZyU3ePp3+KRbhHsY4ogjc+ZQWgVN5h6j8BHW5RII4cFKi6PEKK9QPAcphJVkD0dGpyFnD3VRR0WMvHVjCv9w==", + "dev": true + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -2477,6 +2484,12 @@ "postcss": "^8.1.0" } }, + "node_modules/autosize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/autosize/-/autosize-6.0.1.tgz", + "integrity": "sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==", + "dev": true + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -6028,6 +6041,19 @@ "node": ">=18" } }, + "node_modules/svelte-autosize": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/svelte-autosize/-/svelte-autosize-1.1.5.tgz", + "integrity": "sha512-whiND/GthFDG9Ansvil21qxmFhSMfuooVZPg40sbcLHYKR9srYhnfrP5qdw8MXHAm6DY9g5PawurOAWl34fK7g==", + "dev": true, + "dependencies": { + "@types/autosize": "^4.0.3", + "autosize": "*" + }, + "peerDependencies": { + "svelte": ">=3.0.0" + } + }, "node_modules/svelte-check": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.4.tgz", diff --git a/package.json b/package.json index 6366706e..f8fbbe75 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.3.3", "svelte": "^5.16.0", + "svelte-autosize": "^1.1.5", "svelte-check": "^4.1.4", "tailwind-merge": "^2.6.0", "tailwind-variants": "^0.3.0", diff --git a/src/lib/components/composites/PaperBookmarkButton.svelte b/src/lib/components/composites/PaperBookmarkButton.svelte index 70de99aa..7d700ebf 100644 --- a/src/lib/components/composites/PaperBookmarkButton.svelte +++ b/src/lib/components/composites/PaperBookmarkButton.svelte @@ -7,15 +7,17 @@ import Tooltip from "./Tooltip.svelte"; interface Props { - paperId: number; + loadingPaperId: Promise; isBookmarkedDefault: boolean; } - const { paperId, isBookmarkedDefault }: Props = $props(); + const { loadingPaperId, isBookmarkedDefault }: Props = $props(); let isBookmarked = $state(isBookmarkedDefault); let isHovered = $state(false); const tooltipText = $derived(isBookmarked ? "Remove from Reading List" : "Add to Reading List"); + let paperId: number | undefined = $state(undefined); + loadingPaperId.then((id) => (paperId = id)).catch(() => (paperId = undefined)); const onMouseEnter = () => (isHovered = true); const onMouseLeave = () => (isHovered = false); diff --git a/src/lib/components/composites/input/AbstractToggleableInput.svelte b/src/lib/components/composites/input/AbstractToggleableInput.svelte new file mode 100644 index 00000000..e035549a --- /dev/null +++ b/src/lib/components/composites/input/AbstractToggleableInput.svelte @@ -0,0 +1,28 @@ + + + + diff --git a/src/lib/components/composites/input/ToggleableInput.svelte b/src/lib/components/composites/input/ToggleableInput.svelte new file mode 100644 index 00000000..4dd90ec3 --- /dev/null +++ b/src/lib/components/composites/input/ToggleableInput.svelte @@ -0,0 +1,49 @@ + + + + diff --git a/src/lib/components/composites/input/abstract-max-height-action.ts b/src/lib/components/composites/input/abstract-max-height-action.ts new file mode 100644 index 00000000..4b5d3461 --- /dev/null +++ b/src/lib/components/composites/input/abstract-max-height-action.ts @@ -0,0 +1,59 @@ +import type { Action } from "svelte/action"; + +/** + * This action has the sole purpose of preventing the textarea from growing outside of the card it is in. + * This is achieved by setting the max height of the textarea to the remaining space in the card. + * + * Previous attempts to find a dynamic solution to this problem have failed, so this action is a workaround by + * using hard coded padding values that might change in the future. + */ +export const maxHeight: Action< + HTMLTextAreaElement, + { showButtonBar: boolean; showAdditionalInfos: boolean } +> = (node, { showButtonBar, showAdditionalInfos }) => { + setMaxHeight(node, showButtonBar, showAdditionalInfos); + + // Autosize action sets overflow X to scroll, so we need to reset it + node.addEventListener("input", () => { + node.style.overflowX = "hidden"; + }); + + window.addEventListener("resize", () => { + setMaxHeight(node, showButtonBar, showAdditionalInfos); + }); + + return { + update({ showButtonBar, showAdditionalInfos }) { + setMaxHeight(node, showButtonBar, showAdditionalInfos); + }, + destroy() { + window.removeEventListener("resize", () => { + setMaxHeight(node, showButtonBar, showAdditionalInfos); + }); + }, + }; +}; + +/** + * Sets the max height of the textarea element based on the padding and position of the element. + * + * @param node - the textarea element + * @param showButtonBar - whether the button bar is shown + * @param showAdditionalInfos - whether the additional infos are shown + */ +function setMaxHeight( + node: HTMLTextAreaElement, + showButtonBar: boolean, + showAdditionalInfos: boolean, +) { + // Calculate max height based on the window height + // node.getBoundingClientRect().top gives the top y position of the element i.e. the start of the element + // 113/53 is the sum of all the paddings the button bar and a border + const padding = + (showButtonBar ? 113 : 53) + // 40 Pixels of button bar + 20 pixels of gap + (showAdditionalInfos ? 5 : 0); // somehow there's a 5px change when the additional infos are shown + const maxHeight = window.innerHeight - node.getBoundingClientRect().top - padding; + node.style.maxHeight = `${maxHeight}px`; + node.style.overflowY = "auto"; + node.style.overflowX = "hidden"; +} diff --git a/src/lib/components/composites/navigation-bar/PaperNavigationBar.svelte b/src/lib/components/composites/navigation-bar/PaperNavigationBar.svelte index 9b561041..15291827 100644 --- a/src/lib/components/composites/navigation-bar/PaperNavigationBar.svelte +++ b/src/lib/components/composites/navigation-bar/PaperNavigationBar.svelte @@ -6,12 +6,12 @@ interface Props { user: User; backRef?: string | undefined; - paper: Paper | PaperSpec; + loadingPaper: Promise; } - const { user, backRef, paper }: Props = $props(); + const { user, backRef, loadingPaper }: Props = $props(); - + diff --git a/src/lib/components/composites/navigation-bar/ProjectNavigationBar.svelte b/src/lib/components/composites/navigation-bar/ProjectNavigationBar.svelte index 98084b44..5a770f11 100644 --- a/src/lib/components/composites/navigation-bar/ProjectNavigationBar.svelte +++ b/src/lib/components/composites/navigation-bar/ProjectNavigationBar.svelte @@ -6,31 +6,32 @@ type TabValue = (typeof tabs)[number]["value"]; interface Props { user: User; - project: Project; + projectId: number; + loadingProject: Promise; defaultTabValue: TabValue; } - const { user, project, defaultTabValue }: Props = $props(); + const { user, projectId, loadingProject, defaultTabValue }: Props = $props(); const tabs = [ { value: "dashboard", label: "Dashboard", - href: `/project/${project.id}/dashboard`, + href: `/project/${projectId}/dashboard`, }, { value: "papers", label: "Papers", - href: `/project/${project.id}/papers`, + href: `/project/${projectId}/papers`, }, { value: "statistics", label: "Statistics", - href: `/project/${project.id}/statistics`, + href: `/project/${projectId}/statistics`, }, { value: "settings", label: "Settings", - href: `/project/${project.id}/settings/general`, + href: `/project/${projectId}/settings/general`, }, ] as const; @@ -38,7 +39,7 @@ project.name)} tabs={tabs as unknown as Tab[]} {defaultTabValue} /> diff --git a/src/lib/components/composites/navigation-bar/SimpleNavigationBar.svelte b/src/lib/components/composites/navigation-bar/SimpleNavigationBar.svelte index 187d4b9d..dbd09027 100644 --- a/src/lib/components/composites/navigation-bar/SimpleNavigationBar.svelte +++ b/src/lib/components/composites/navigation-bar/SimpleNavigationBar.svelte @@ -2,18 +2,31 @@ import NavigationBar from "./NavigationBar.svelte"; import type { Tab } from "$lib/components/composites/navigation-bar/types"; import type { User } from "$lib/model/backend"; + import Skeleton from "$lib/components/primitives/skeleton/skeleton.svelte"; interface Props { user: User; backRef?: string | undefined; - title: string; + loadingTitle: Promise; tabs?: Tab[] | undefined; defaultTabValue?: (typeof tabs)[number]["value"] | undefined; } - const { user, backRef = undefined, title, tabs = [], defaultTabValue = "" }: Props = $props(); + const { + user, + backRef = undefined, + loadingTitle, + tabs = [], + defaultTabValue = "", + }: Props = $props(); -

{title}

+ {#await loadingTitle} + + {:then title} +

{title}

+ {:catch} +

Error

+ {/await}
diff --git a/src/lib/components/composites/paper-components/PaperInfo.svelte b/src/lib/components/composites/paper-components/PaperInfo.svelte index c7697640..27fd48e7 100644 --- a/src/lib/components/composites/paper-components/PaperInfo.svelte +++ b/src/lib/components/composites/paper-components/PaperInfo.svelte @@ -1,27 +1,36 @@ -
-
- {#if "id" in paper} -
#{paper.id}
- {/if} -

{paper.title}

+{#await loadingPaper} +
+ +
-
- {#if paper.authors.length > 0} - {paper.authors.map((a) => `${a.firstName} ${a.lastName}`).join(", ")} - {:else} - unknown authors - {/if} +{:then paper} +
+
+ {#if "id" in paper} +
#{paper.id}
+ {/if} +

{paper.title}

+
+
+ {#if paper.authors.length > 0} + {getNames(paper.authors)} + {:else} + unknown authors + {/if} +
-
+{:catch} + Failed to load paper +{/await} diff --git a/src/lib/components/composites/paper-components/PaperListEntry.svelte b/src/lib/components/composites/paper-components/PaperListEntry.svelte index e2fedee2..11b19da3 100644 --- a/src/lib/components/composites/paper-components/PaperListEntry.svelte +++ b/src/lib/components/composites/paper-components/PaperListEntry.svelte @@ -80,7 +80,7 @@ Usage: ? `border-l-4 ${reviewDecisionColor[paper.reviewData?.finalDecision ?? 'unreviewed']}` : ''} rounded-md px-3 py-1.5" > - +
{#if paper.reviewData !== undefined && showReviewStatus} {#each paper.reviewData.reviews as review} diff --git a/src/lib/components/composites/paper-components/paper-view/PaperDetail.svelte b/src/lib/components/composites/paper-components/paper-view/PaperDetail.svelte new file mode 100644 index 00000000..134b2429 --- /dev/null +++ b/src/lib/components/composites/paper-components/paper-view/PaperDetail.svelte @@ -0,0 +1,43 @@ + + + +
+ + {key} + {#await loadingPaper} +
+ +
+ {:then} + + {:catch} + Couldn't load {key} + {/await} +
diff --git a/src/lib/components/composites/paper-components/paper-view/PaperView.svelte b/src/lib/components/composites/paper-components/paper-view/PaperView.svelte index fc0a47e9..3ae198b1 100644 --- a/src/lib/components/composites/paper-components/paper-view/PaperView.svelte +++ b/src/lib/components/composites/paper-components/paper-view/PaperView.svelte @@ -11,24 +11,28 @@ interface Props { user: User; - paper: Paper; - isPaperBookmarked?: boolean; - showButtonBar: boolean; + loadingPaper: Promise; + showButtonBar?: boolean; backRef: string; userConfig: { isReviewMode: boolean; showMaybeButton: boolean; }; + allowEditModeToggle?: boolean; + startInEditMode?: boolean; } const { user, - paper, - isPaperBookmarked = false, - showButtonBar, + loadingPaper, + showButtonBar = false, backRef, userConfig, + allowEditModeToggle = false, + startInEditMode = false, }: Props = $props(); + + let loadingPaperId = loadingPaper.then((paper) => paper.id);
- - + + +
- +
{#if showButtonBar} @@ -72,11 +84,11 @@ Usage:
- + {#if userConfig.showMaybeButton} - + {/if} - +
{/if} diff --git a/src/lib/components/composites/paper-components/paper-view/cards/PaperCard.svelte b/src/lib/components/composites/paper-components/paper-view/cards/PaperCard.svelte index ba997146..08f9083e 100644 --- a/src/lib/components/composites/paper-components/paper-view/cards/PaperCard.svelte +++ b/src/lib/components/composites/paper-components/paper-view/cards/PaperCard.svelte @@ -3,13 +3,17 @@ import UnderlineTabsList from "$lib/components/composites/tabs/UnderlineTabsList.svelte"; import * as Card from "$lib/components/primitives/card/index.js"; import * as Tabs from "$lib/components/primitives/tabs/index.js"; + import { cn } from "$lib/utils/shadcn-helper"; + import type { WithElementRef } from "bits-ui"; import type { Snippet } from "svelte"; + import type { HTMLAttributes } from "svelte/elements"; - interface Props { + type Props = WithElementRef> & { tabs: Tab[]; children: Snippet; - } - const { tabs, children }: Props = $props(); + }; + + const { tabs, children, class: className, ...restProps }: Props = $props(); @@ -32,11 +36,14 @@ Usage: ``` --> - -
- + +
+ - + {@render children()} diff --git a/src/lib/components/composites/paper-components/paper-view/cards/PaperCardContent.svelte b/src/lib/components/composites/paper-components/paper-view/cards/PaperCardContent.svelte index ceff443a..99d7ef6c 100644 --- a/src/lib/components/composites/paper-components/paper-view/cards/PaperCardContent.svelte +++ b/src/lib/components/composites/paper-components/paper-view/cards/PaperCardContent.svelte @@ -22,8 +22,8 @@ Usage: ``` --> - -
+ +
{@render children()}
diff --git a/src/lib/components/composites/paper-components/paper-view/cards/PaperDetailsCard.svelte b/src/lib/components/composites/paper-components/paper-view/cards/PaperDetailsCard.svelte index 01a9b6cf..d28f02c5 100644 --- a/src/lib/components/composites/paper-components/paper-view/cards/PaperDetailsCard.svelte +++ b/src/lib/components/composites/paper-components/paper-view/cards/PaperDetailsCard.svelte @@ -1,11 +1,88 @@ - + - Will be implemented in #41. +
+
+

General Information

+ {#if allowEditModeToggle} + (areDetailsInEditMode = !areDetailsInEditMode)} + class="hover:cursor-pointer select-none" + /> + {/if} +
+
+ {#each Object.entries(basicInfos.value) as [key, value]} + + {/each} + {#if showAdditionalInfos} + {#each Object.entries(additionalInfos.value) as [key, value]} + + {/each} + {/if} +
+
+ +
+
+
+
+

Abstract

+ {#if allowEditModeToggle} + (isAbstractInEditMode = !isAbstractInEditMode)} + class="hover:cursor-pointer select-none" + /> + {/if} +
+ {#await loadingPaper} + {#each [100, 95, 70, 82, 50, 75, 90] as width} + + {/each} + {:then paper} + + {:catch} + Couldn't load Abstract + {/await} +
- Will be implemented in #98. + + Will be implemented in + + #98 + + . +
diff --git a/src/lib/components/composites/paper-components/paper-view/decision-buttons/AcceptButton.svelte b/src/lib/components/composites/paper-components/paper-view/decision-buttons/AcceptButton.svelte index 00389c34..61ff237b 100644 --- a/src/lib/components/composites/paper-components/paper-view/decision-buttons/AcceptButton.svelte +++ b/src/lib/components/composites/paper-components/paper-view/decision-buttons/AcceptButton.svelte @@ -1,14 +1,22 @@ - + {#snippet buttonContent()}

Accept

Ctrl+A

diff --git a/src/lib/components/composites/paper-components/paper-view/decision-buttons/DecisionButton.svelte b/src/lib/components/composites/paper-components/paper-view/decision-buttons/DecisionButton.svelte index 634c284a..8cf51460 100644 --- a/src/lib/components/composites/paper-components/paper-view/decision-buttons/DecisionButton.svelte +++ b/src/lib/components/composites/paper-components/paper-view/decision-buttons/DecisionButton.svelte @@ -8,16 +8,29 @@ type Props = WithElementRef & { buttonContent: Snippet; tooltipContent: Snippet; - onClick: () => void; + loadingPaperId: Promise; + onClick: (paperId: number) => void; }; const { buttonContent, tooltipContent, + loadingPaperId, onClick, class: className, ...restProps }: Props = $props(); + + let paperId = $state(undefined); + loadingPaperId.then((id) => (paperId = id)).catch(() => (paperId = undefined)); + + function onButtonClick() { + if (paperId) { + onClick(paperId); + } else { + console.error("Paper ID is not set"); + } + } @@ -29,7 +42,11 @@ Rather use `AcceptButton` or `DeclineButton` or `MaybeButton` instead of this co Usage: ```svelte - console.log("clicked button")}> + console.log("clicked button")} + {loadingPaperId} + > {#snippet buttonContent()}

This is a button

{/snippet} @@ -43,7 +60,7 @@ Usage: class={cn("text-primary max-w-[20rem] shadow-lg flex-grow-1000", className)} trigger={buttonContent} content={tooltipContent} - onclick={onClick} + onclick={onButtonClick} {...restProps} data-testid="decision-button" > diff --git a/src/lib/components/composites/paper-components/paper-view/decision-buttons/DeclineButton.svelte b/src/lib/components/composites/paper-components/paper-view/decision-buttons/DeclineButton.svelte index 7534ad50..11b3be15 100644 --- a/src/lib/components/composites/paper-components/paper-view/decision-buttons/DeclineButton.svelte +++ b/src/lib/components/composites/paper-components/paper-view/decision-buttons/DeclineButton.svelte @@ -1,14 +1,22 @@ - + {#snippet buttonContent()}

Decline

Ctrl+D

diff --git a/src/lib/components/composites/paper-components/paper-view/decision-buttons/MaybeButton.svelte b/src/lib/components/composites/paper-components/paper-view/decision-buttons/MaybeButton.svelte index 378a0c3b..b3cb3df1 100644 --- a/src/lib/components/composites/paper-components/paper-view/decision-buttons/MaybeButton.svelte +++ b/src/lib/components/composites/paper-components/paper-view/decision-buttons/MaybeButton.svelte @@ -1,14 +1,22 @@ - + {#snippet buttonContent()}

Maybe

Ctrl+S

diff --git a/src/lib/components/composites/tabs/UnderlineTabsList.svelte b/src/lib/components/composites/tabs/UnderlineTabsList.svelte index 93806f46..3b5ae29c 100644 --- a/src/lib/components/composites/tabs/UnderlineTabsList.svelte +++ b/src/lib/components/composites/tabs/UnderlineTabsList.svelte @@ -33,9 +33,9 @@ Usage: {#each tabs as tab} - {tab.label} +
{tab.label}
{/each} diff --git a/src/lib/components/primitives/skeleton/skeleton.svelte b/src/lib/components/primitives/skeleton/skeleton.svelte index a5661395..9f3bb40d 100644 --- a/src/lib/components/primitives/skeleton/skeleton.svelte +++ b/src/lib/components/primitives/skeleton/skeleton.svelte @@ -13,5 +13,6 @@
diff --git a/src/lib/controller/paper-controller.ts b/src/lib/controller/paper-controller.ts index aa6f127a..23fa5a28 100644 --- a/src/lib/controller/paper-controller.ts +++ b/src/lib/controller/paper-controller.ts @@ -10,7 +10,7 @@ export class PaperController implements IPaperController { } async get(): Promise { - throw new Error("Method not implemented."); + return this.client.get().then((response) => response.json()); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/lib/controller/project-controller.ts b/src/lib/controller/project-controller.ts index c3a5250a..1283bba3 100644 --- a/src/lib/controller/project-controller.ts +++ b/src/lib/controller/project-controller.ts @@ -14,7 +14,7 @@ export class ProjectController implements IProjectController { } async get(): Promise { - throw new Error("Method not implemented."); + return this.client.get().then((response) => response.json()); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/lib/resource.svelte.ts b/src/lib/resource.svelte.ts new file mode 100644 index 00000000..fa1a7f6c --- /dev/null +++ b/src/lib/resource.svelte.ts @@ -0,0 +1,28 @@ +export const resource = ( + loadingResource: Promise, + options: { + initialValue: TValue; + onSuccess: (value: TPromise) => TValue; + onErrorValue?: TValue; + }, +) => { + const { initialValue, onSuccess, onErrorValue } = options; + + const rune = $state<{ value: TValue }>({ + value: initialValue, + }); + + $effect(() => { + loadingResource + .then((value) => { + rune.value = onSuccess(value); + }) + .catch(() => { + if (onErrorValue) { + rune.value = onErrorValue; + } + }); + }); + + return rune; +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index bb9cdf14..5abc14fc 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -13,7 +13,7 @@ SnowballR - +
Archived Projects - + diff --git a/src/routes/componentsusage/+page.svelte b/src/routes/componentsusage/+page.svelte index 9ec30e9f..3b10012b 100644 --- a/src/routes/componentsusage/+page.svelte +++ b/src/routes/componentsusage/+page.svelte @@ -15,7 +15,12 @@ Component Usage Demo
- + console.log(input)} /> console.log(input)} /> diff --git a/src/routes/invitations/+page.svelte b/src/routes/invitations/+page.svelte index 649997fe..038a5be7 100644 --- a/src/routes/invitations/+page.svelte +++ b/src/routes/invitations/+page.svelte @@ -8,4 +8,4 @@ Project Invitations - + diff --git a/src/routes/paper/[paperId]/+page.svelte b/src/routes/paper/[paperId]/+page.svelte index 8d14d36d..892804cb 100644 --- a/src/routes/paper/[paperId]/+page.svelte +++ b/src/routes/paper/[paperId]/+page.svelte @@ -2,16 +2,21 @@ import PaperView from "$lib/components/composites/paper-components/paper-view/PaperView.svelte"; const { data } = $props(); - const { user, paper } = data; + const { user, loadingPaper } = data; - {paper.title} + {#await loadingPaper} + Loading paper... + {:then paper} + {paper.title} + {:catch} + Failed loading paper + {/await} diff --git a/src/routes/paper/[paperId]/+page.ts b/src/routes/paper/[paperId]/+page.ts index 1179faae..48f3828b 100644 --- a/src/routes/paper/[paperId]/+page.ts +++ b/src/routes/paper/[paperId]/+page.ts @@ -1,29 +1,14 @@ -import type { Paper } from "$lib/model/backend"; +import { PaperController } from "$lib/controller/paper-controller"; import type { PageLoad } from "./$types"; export const load: PageLoad = ({ params }) => { const paperId = Number(params.paperId); - if (Number.isNaN(paperId)) { - throw new Error(`Invalid paperId ${params.paperId}`); - } - const paper: Paper = { - doi: "Doi", - id: paperId, - title: "Field-Sensitive Pointer Analysis for Static Dataflow in the R Programming Language", - abstrakt: "Abstrakt", - year: 2015, - type: "Paper", - authors: [ - { id: 0, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 1, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 2, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 3, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 4, firstName: "Foo", lastName: "Bar", orcid: "" }, - ], - backwardReferencedPaperIds: [], - forwardReferencedPaperIds: [], - }; + const loadingPaper = new PaperController(paperId).get(); + + // attach noop-catch to handle promise rejection correctly (see https://svelte.dev/docs/kit/load#Streaming-with-promises) + loadingPaper.catch(() => {}); + return { - paper, + loadingPaper, }; }; diff --git a/src/routes/project/[projectId]/+layout.ts b/src/routes/project/[projectId]/+layout.ts index 98afd8b2..96aa0597 100644 --- a/src/routes/project/[projectId]/+layout.ts +++ b/src/routes/project/[projectId]/+layout.ts @@ -1,23 +1,15 @@ -import type { Project } from "$lib/model/backend"; +import { ProjectController } from "$lib/controller/project-controller"; import type { LayoutLoad } from "./$types"; export const load: LayoutLoad = async ({ params }) => { const projectId = Number(params.projectId); - if (Number.isNaN(projectId)) { - throw new Error(`Invalid projectId ${params.projectId}`); - } - const project: Project = { - id: projectId, - name: "Project " + projectId, - reviewDecisionMatrix: { - numberOfReviewers: 1, - patterns: new Map(), - }, - similarityThreshold: 0, - paperFetchApis: [], - archived: false, - }; + const loadingProject = new ProjectController(projectId).get(); + + // attach noop-catch to handle promise rejection correctly (see https://svelte.dev/docs/kit/load#Streaming-with-promises) + loadingProject.catch(() => {}); + return { - project, + projectId, + loadingProject, }; }; diff --git a/src/routes/project/[projectId]/dashboard/+page.svelte b/src/routes/project/[projectId]/dashboard/+page.svelte index 11ce4176..4ead8561 100644 --- a/src/routes/project/[projectId]/dashboard/+page.svelte +++ b/src/routes/project/[projectId]/dashboard/+page.svelte @@ -2,12 +2,16 @@ import ProjectNavigationBar from "$lib/components/composites/navigation-bar/ProjectNavigationBar.svelte"; const { data } = $props(); - const { user, project } = data; + const { user, projectId, loadingProject } = data; - {project.name} + {#await loadingProject} + Loading Project... + {:then project} + {project.name} + {:catch} + Project Dashboard + {/await} - - -

Project {project.id} Dashboard

+ diff --git a/src/routes/project/[projectId]/paper/[paperId]/+page.svelte b/src/routes/project/[projectId]/paper/[paperId]/+page.svelte index 75da62fd..c68fea4b 100644 --- a/src/routes/project/[projectId]/paper/[paperId]/+page.svelte +++ b/src/routes/project/[projectId]/paper/[paperId]/+page.svelte @@ -2,16 +2,24 @@ import PaperView from "$lib/components/composites/paper-components/paper-view/PaperView.svelte"; const { data } = $props(); - const { user, project, paper, isReviewMode } = data; + const { user, projectId, loadingProject, loadingPaper, isReviewMode } = data; - {paper.title} | {project.name} + {#await Promise.all([loadingProject, loadingPaper])} + Loading paper and project... + {:then [project, paper]} + {paper.title} | {project.name} + {:catch} + Failed loading data + {/await} + diff --git a/src/routes/project/[projectId]/paper/[paperId]/+page.ts b/src/routes/project/[projectId]/paper/[paperId]/+page.ts index d6f31cf2..517f0772 100644 --- a/src/routes/project/[projectId]/paper/[paperId]/+page.ts +++ b/src/routes/project/[projectId]/paper/[paperId]/+page.ts @@ -1,30 +1,15 @@ -import type { Paper } from "$lib/model/backend"; +import { PaperController } from "$lib/controller/paper-controller"; import type { PageLoad } from "./$types"; export const load: PageLoad = ({ params }) => { const paperId = Number(params.paperId); - if (Number.isNaN(paperId)) { - throw new Error(`Invalid paperId ${params.paperId}`); - } - const paper: Paper = { - doi: "Doi", - id: paperId, - title: "Field-Sensitive Pointer Analysis for Static Dataflow in the R Programming Language", - abstrakt: "Abstrakt", - year: 2015, - type: "Paper", - authors: [ - { id: 0, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 1, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 2, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 3, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 4, firstName: "Foo", lastName: "Bar", orcid: "" }, - ], - backwardReferencedPaperIds: [], - forwardReferencedPaperIds: [], - }; + const loadingPaper = new PaperController(paperId).get(); + + // attach noop-catch to handle promise rejection correctly (see https://svelte.dev/docs/kit/load#Streaming-with-promises) + loadingPaper.catch(() => {}); + return { - paper, + loadingPaper, isReviewMode: true, }; }; diff --git a/src/routes/project/[projectId]/paper/new/+page.svelte b/src/routes/project/[projectId]/paper/new/+page.svelte index 9786b2e6..89eeae80 100644 --- a/src/routes/project/[projectId]/paper/new/+page.svelte +++ b/src/routes/project/[projectId]/paper/new/+page.svelte @@ -3,26 +3,37 @@ import type { PaperSpec } from "$lib/model/backend"; const { data } = $props(); - const { user, project } = data; - const paper: PaperSpec = { - doi: "Doi", - title: "Field-Sensitive Pointer Analysis for Static Dataflow in the R Programming Language", - abstrakt: "Abstrakt", - year: 2015, - type: "Paper", - authors: [ - { id: 0, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 1, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 2, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 3, firstName: "Foo", lastName: "Bar", orcid: "" }, - { id: 4, firstName: "Foo", lastName: "Bar", orcid: "" }, - ], + const { user, projectId, loadingProject } = data; + + const emptyPaper: PaperSpec = { + doi: "", + title: "Empty Title", + abstrakt: "", + year: new Date().getFullYear(), + type: "", + authors: [], backwardReferencedPaperIds: [], forwardReferencedPaperIds: [], }; - Add Paper | {project.name} + {#await loadingProject} + Loading Project... + {:then project} + Add Paper | {project.name} + {:catch} + Add Paper + {/await} - +{#await loadingProject} +

Loading...

+{:then} + +{:catch error} +

{error.message}

+{/await} diff --git a/src/routes/project/[projectId]/papers/+page.svelte b/src/routes/project/[projectId]/papers/+page.svelte index 5cb5ff57..10467a3d 100644 --- a/src/routes/project/[projectId]/papers/+page.svelte +++ b/src/routes/project/[projectId]/papers/+page.svelte @@ -2,12 +2,16 @@ import ProjectNavigationBar from "$lib/components/composites/navigation-bar/ProjectNavigationBar.svelte"; let { data } = $props(); - const { user, project } = data; + const { user, projectId, loadingProject } = data; - Papers | {project.name} + {#await loadingProject} + Loading Project... + {:then project} + Papers | {project.name} + {:catch} + Papers + {/await} - - -

Project {project.id} Papers

+ diff --git a/src/routes/project/[projectId]/settings/+layout.svelte b/src/routes/project/[projectId]/settings/+layout.svelte index 9a2056d8..6065a79a 100644 --- a/src/routes/project/[projectId]/settings/+layout.svelte +++ b/src/routes/project/[projectId]/settings/+layout.svelte @@ -2,11 +2,9 @@ import ProjectNavigationBar from "$lib/components/composites/navigation-bar/ProjectNavigationBar.svelte"; const { children, data } = $props(); - const { user, project } = data; + const { user, projectId, loadingProject } = data; - - -

Project {project.id} Settings

+ {@render children()} diff --git a/src/routes/project/[projectId]/settings/general/+page.svelte b/src/routes/project/[projectId]/settings/general/+page.svelte index a6a81940..a2d6c096 100644 --- a/src/routes/project/[projectId]/settings/general/+page.svelte +++ b/src/routes/project/[projectId]/settings/general/+page.svelte @@ -1,9 +1,14 @@ - General | Settings | {project.name} + {#await loadingProject} + Loading Project... + {:then project} + General | Settings | {project.name} + {:catch} + General | Settings + {/await} -

Project {project.id} Settings - General

diff --git a/src/routes/project/[projectId]/settings/members/+page.svelte b/src/routes/project/[projectId]/settings/members/+page.svelte index 834bef0b..809f990c 100644 --- a/src/routes/project/[projectId]/settings/members/+page.svelte +++ b/src/routes/project/[projectId]/settings/members/+page.svelte @@ -1,9 +1,14 @@ - Members | Settings | {project.name} + {#await loadingProject} + Loading Project... + {:then project} + Members | Settings | {project.name} + {:catch} + Members | Settings + {/await} -

Project {project.id} Settings - Members

diff --git a/src/routes/project/[projectId]/settings/review/+page.svelte b/src/routes/project/[projectId]/settings/review/+page.svelte index bcb358f6..b40acf66 100644 --- a/src/routes/project/[projectId]/settings/review/+page.svelte +++ b/src/routes/project/[projectId]/settings/review/+page.svelte @@ -1,9 +1,14 @@ - Review | Settings | {project.name} + {#await loadingProject} + Loading Project... + {:then project} + Review | Settings | {project.name} + {:catch} + Review | Settings + {/await} -

Project {project.id} Settings - Review

diff --git a/src/routes/project/[projectId]/settings/slr/+page.svelte b/src/routes/project/[projectId]/settings/slr/+page.svelte index 80bd5fbb..dfc41415 100644 --- a/src/routes/project/[projectId]/settings/slr/+page.svelte +++ b/src/routes/project/[projectId]/settings/slr/+page.svelte @@ -1,9 +1,14 @@ - SLR | Settings | {project.name} + {#await loadingProject} + Loading Project... + {:then project} + SLR | Settings | {project.name} + {:catch} + SLR | Settings + {/await} -

Project {project.id} Settings - SLR

diff --git a/src/routes/project/[projectId]/statistics/+page.svelte b/src/routes/project/[projectId]/statistics/+page.svelte index 786c5fd0..02803450 100644 --- a/src/routes/project/[projectId]/statistics/+page.svelte +++ b/src/routes/project/[projectId]/statistics/+page.svelte @@ -2,12 +2,16 @@ import ProjectNavigationBar from "$lib/components/composites/navigation-bar/ProjectNavigationBar.svelte"; const { data } = $props(); - const { user, project } = data; + const { user, projectId, loadingProject } = data; - Statistics | {project.name} + {#await loadingProject} + Loading Project... + {:then project} + Statistics | {project.name} + {:catch} + Statistics + {/await} - - -

Project {project.id} Statistics

+ diff --git a/src/routes/readinglist/+page.svelte b/src/routes/readinglist/+page.svelte index f552000f..6b8690bc 100644 --- a/src/routes/readinglist/+page.svelte +++ b/src/routes/readinglist/+page.svelte @@ -8,4 +8,4 @@ Reading List - + diff --git a/src/routes/settings/+layout.svelte b/src/routes/settings/+layout.svelte index 7b904d8e..7f7d5209 100644 --- a/src/routes/settings/+layout.svelte +++ b/src/routes/settings/+layout.svelte @@ -5,6 +5,6 @@ const { user } = data; - + {@render children()} diff --git a/tests/integration/input/toggleable-input.test.ts b/tests/integration/input/toggleable-input.test.ts new file mode 100644 index 00000000..7cbbe811 --- /dev/null +++ b/tests/integration/input/toggleable-input.test.ts @@ -0,0 +1,50 @@ +import ToggleableInput from "$lib/components/composites/input/ToggleableInput.svelte"; +import { render, screen } from "@testing-library/svelte"; +import { keyboard } from "@testing-library/user-event/dist/cjs/setup/directApi.js"; +import { describe, expect, test } from "vitest"; + +describe("ToggleableInput", () => { + test("When isEditable is set to true, then input border is shown and content can be edited", async () => { + render(ToggleableInput, { + target: document.body, + props: { + isEditable: true, + value: "", + }, + }); + + const input = screen.getByRole("textbox"); + expect(input).toBeInTheDocument(); + + expect(input.classList.contains("border")).toBe(true); + expect(input.classList.contains("border-input")).toBe(true); + expect(input.classList.contains("rounded-md")).toBe(true); + expect(input.classList.contains("border-transparent")).toBe(false); + + expect(input).not.toHaveAttribute("readonly"); + + await keyboard("Test"); + + expect(input).not.toHaveValue("Test"); + expect(input).toHaveValue(""); + }); + + test("When isEditable is set to false, then input border is not shown and content cannot be edited", () => { + render(ToggleableInput, { + target: document.body, + props: { + isEditable: false, + }, + }); + + const input = screen.getByRole("textbox"); + expect(input).toBeInTheDocument(); + + expect(input.classList.contains("border")).toBe(true); + expect(input.classList.contains("border-input")).toBe(false); + expect(input.classList.contains("rounded-md")).toBe(false); + expect(input.classList.contains("border-transparent")).toBe(true); + + expect(input).toHaveAttribute("readonly"); + }); +}); diff --git a/tests/integration/navigation-bar/paper-navigation-bar.test.ts b/tests/integration/navigation-bar/paper-navigation-bar.test.ts index 1a77f19a..64c4fe76 100644 --- a/tests/integration/navigation-bar/paper-navigation-bar.test.ts +++ b/tests/integration/navigation-bar/paper-navigation-bar.test.ts @@ -1,7 +1,8 @@ import { expect, test, describe, assert } from "vitest"; import PaperNavigationBar from "$lib/components/composites/navigation-bar/PaperNavigationBar.svelte"; import { render, screen } from "@testing-library/svelte"; -import { Authors, createPaper, Users } from "../../model-builder"; +import { Authors, createLoadingPaper, Users } from "../../model-builder"; +import { waitForComponentLoading } from "../test-helper"; describe("PaperNavigationBar", () => { test("When all props are provided, then whole navigation bar is shown", async () => { @@ -10,7 +11,7 @@ describe("PaperNavigationBar", () => { props: { user: Users.johnDoe, backRef: "/", - paper: createPaper({ + loadingPaper: createLoadingPaper({ id: 123, title: "Example Paper Title", authors: [Authors.johnDoe, Authors.janeDoe], @@ -18,6 +19,8 @@ describe("PaperNavigationBar", () => { }, }); + await waitForComponentLoading(); + const header = screen.getByRole("banner"); expect(header).toBeInTheDocument(); @@ -55,13 +58,15 @@ describe("PaperNavigationBar", () => { props: { user: Users.johnDoe, backRef: "/", - paper: createPaper({ + loadingPaper: createLoadingPaper({ title: "Example Paper Title", authors: [Authors.johnDoe, Authors.janeDoe], }), }, }); + await waitForComponentLoading(); + assert.throws(() => screen.getByText("#123")); }); @@ -71,7 +76,7 @@ describe("PaperNavigationBar", () => { props: { user: Users.johnDoe, backRef: "/", - paper: createPaper({ + loadingPaper: createLoadingPaper({ id: 123, title: "Example Paper Title", authors: [], @@ -79,6 +84,8 @@ describe("PaperNavigationBar", () => { }, }); + await waitForComponentLoading(); + const paperAuthors = screen.getByText("unknown authors"); expect(paperAuthors).toBeInTheDocument(); }); diff --git a/tests/integration/navigation-bar/project-navigation-bar.test.ts b/tests/integration/navigation-bar/project-navigation-bar.test.ts index d299ea63..5aa8909b 100644 --- a/tests/integration/navigation-bar/project-navigation-bar.test.ts +++ b/tests/integration/navigation-bar/project-navigation-bar.test.ts @@ -1,7 +1,8 @@ import { assert, expect, test, describe } from "vitest"; import ProjectNavigationBar from "$lib/components/composites/navigation-bar/ProjectNavigationBar.svelte"; import { render, screen } from "@testing-library/svelte"; -import { createProject, createUser } from "../../model-builder"; +import { createLoadingProject, createUser } from "../../model-builder"; +import { waitForComponentLoading } from "../test-helper"; describe("ProjectNavigationBar", () => { test("When all props are provided, then whole navigation bar is shown", async () => { @@ -12,7 +13,8 @@ describe("ProjectNavigationBar", () => { firstName: "John", lastName: "Doe", }), - project: createProject({ + projectId: 123, + loadingProject: createLoadingProject({ id: 123, name: "Example Project Title", }), @@ -20,6 +22,8 @@ describe("ProjectNavigationBar", () => { }, }); + await waitForComponentLoading(); + const linkTags = screen.getAllByRole("link"); expect(linkTags).toHaveLength(5); diff --git a/tests/integration/navigation-bar/simple-navigation-bar.test.ts b/tests/integration/navigation-bar/simple-navigation-bar.test.ts index ff545cb2..06634bce 100644 --- a/tests/integration/navigation-bar/simple-navigation-bar.test.ts +++ b/tests/integration/navigation-bar/simple-navigation-bar.test.ts @@ -2,9 +2,10 @@ import { expect, test, describe } from "vitest"; import SimpleNavigationBar from "$lib/components/composites/navigation-bar/SimpleNavigationBar.svelte"; import { render, screen } from "@testing-library/svelte"; import { createUser } from "../../model-builder"; +import { waitForComponentLoading } from "../test-helper"; describe("SimpleNavigationBar", () => { - test("When all props are provided, then whole navigation bar is shown", () => { + test("When all props are provided, then whole navigation bar is shown", async () => { render(SimpleNavigationBar, { target: document.body, props: { @@ -13,10 +14,12 @@ describe("SimpleNavigationBar", () => { lastName: "Doe", }), backRef: "/", - title: "Simple Navigation Bar", + loadingTitle: Promise.resolve("Simple Navigation Bar"), }, }); + await waitForComponentLoading(); + // Title is shown const title = screen.getByText("Simple Navigation Bar"); expect(title).toBeInTheDocument(); diff --git a/tests/integration/paper-bookmark-button.test.ts b/tests/integration/paper-bookmark-button.test.ts index 6e714c0d..cea76c94 100644 --- a/tests/integration/paper-bookmark-button.test.ts +++ b/tests/integration/paper-bookmark-button.test.ts @@ -8,7 +8,7 @@ describe("PaperBookmarkButton", () => { render(PaperBookmarkButton, { target: document.body, props: { - paperId: 1, + loadingPaperId: Promise.resolve(1), isBookmarkedDefault: false, }, }); @@ -22,7 +22,7 @@ describe("PaperBookmarkButton", () => { render(PaperBookmarkButton, { target: document.body, props: { - paperId: 1, + loadingPaperId: Promise.resolve(1), isBookmarkedDefault: true, }, }); @@ -37,7 +37,7 @@ describe("PaperBookmarkButton", () => { render(PaperBookmarkButton, { target: document.body, props: { - paperId: 1, + loadingPaperId: Promise.resolve(1), isBookmarkedDefault: false, }, }); @@ -59,7 +59,7 @@ describe("PaperBookmarkButton", () => { render(PaperBookmarkButton, { target: document.body, props: { - paperId: 1, + loadingPaperId: Promise.resolve(1), isBookmarkedDefault: true, }, }); diff --git a/tests/integration/paper-components/paper-list-entry.test.ts b/tests/integration/paper-components/paper-list-entry.test.ts index 210944bc..20a25774 100644 --- a/tests/integration/paper-components/paper-list-entry.test.ts +++ b/tests/integration/paper-components/paper-list-entry.test.ts @@ -3,9 +3,10 @@ import PaperEntry from "$lib/components/composites/paper-components/PaperListEnt import { render, screen } from "@testing-library/svelte"; import userEvent from "@testing-library/user-event"; import { createPaper, Users } from "../../model-builder"; +import { waitForComponentLoading } from "../test-helper"; describe("PaperListEntryComponent", () => { - test("When all required props are provided, then the paper list entry is completely shown (without review information)", () => { + test("When all required props are provided, then the paper list entry is completely shown (without review information)", async () => { render(PaperEntry, { props: { paper: createPaper({ @@ -16,6 +17,8 @@ describe("PaperListEntryComponent", () => { }, }); + await waitForComponentLoading(); + expect(screen.getByText("#0")).toBeInTheDocument(); expect(screen.getByText("Test Title")).toBeInTheDocument(); expect(screen.getByText("John Doe, Jane Doe")).toBeInTheDocument(); @@ -26,7 +29,7 @@ describe("PaperListEntryComponent", () => { expect(screen.getByRole("button").childElementCount).toBe(1); }); - test("When review information are provided, but should not be shown, then the paper list entry is completely shown without review information", () => { + test("When review information are provided, but should not be shown, then the paper list entry is completely shown without review information", async () => { render(PaperEntry, { props: { paper: createPaper({ @@ -38,6 +41,8 @@ describe("PaperListEntryComponent", () => { }, }); + await waitForComponentLoading(); + expect(screen.getByText("#0")).toBeInTheDocument(); expect(screen.getByText("Test Title")).toBeInTheDocument(); expect(screen.getByText("John Doe, Jane Doe")).toBeInTheDocument(); @@ -48,7 +53,7 @@ describe("PaperListEntryComponent", () => { expect(screen.getByRole("button").childElementCount).toBe(1); }); - test("When review information are provided and should be shown, then the paper list entry is completely shown with review information", () => { + test("When review information are provided and should be shown, then the paper list entry is completely shown with review information", async () => { render(PaperEntry, { props: { paper: createPaper({ @@ -60,6 +65,8 @@ describe("PaperListEntryComponent", () => { }, }); + await waitForComponentLoading(); + expect(screen.getByText("#0")).toBeInTheDocument(); expect(screen.getByText("Test Title")).toBeInTheDocument(); expect(screen.getByText("John Doe, Jane Doe")).toBeInTheDocument(); @@ -83,6 +90,8 @@ describe("PaperListEntryComponent", () => { }, }); + await waitForComponentLoading(); + await userEvent.dblClick(screen.getByRole("button")); expect(onClickExecuted).equal(false); diff --git a/tests/integration/paper-components/paper-view/cards/paper-details-card.test.ts b/tests/integration/paper-components/paper-view/cards/paper-details-card.test.ts new file mode 100644 index 00000000..1f8a9880 --- /dev/null +++ b/tests/integration/paper-components/paper-view/cards/paper-details-card.test.ts @@ -0,0 +1,208 @@ +import PaperDetailsCard from "$lib/components/composites/paper-components/paper-view/cards/PaperDetailsCard.svelte"; +import { render, screen, waitFor } from "@testing-library/svelte"; +import { describe, expect, test } from "vitest"; +import { createPaper } from "../../../../model-builder"; +import { waitForComponentLoading } from "../../../test-helper"; +import userEvent from "@testing-library/user-event"; +import type { Paper } from "$lib/model/backend"; + +describe("PaperDetailsCard", () => { + test("When props are provided, then component is shown", async () => { + render(PaperDetailsCard, { + target: document.body, + props: { + loadingPaper: Promise.resolve(createPaper()), + allowEditModeToggle: true, + startInEditMode: false, + showButtonBar: true, + }, + }); + + await waitForComponentLoading(); + + const card = screen.getByTestId("paper-details-card"); + expect(card).toBeInTheDocument(); + + // edit mode can be toggled + const editButtons = document.getElementsByTagName("svg"); + let editButtonCount = 0; + for (const editButton of editButtons) { + if (!editButton.classList.contains("lucide-pencil")) { + continue; + } + + expect(editButton).toBeInTheDocument(); + editButtonCount++; + } + expect(editButtonCount).toBe(2); + + // paper details are in read-only mode + const toggleableInputs = screen.queryAllByTestId("toggleable-input"); + expect(toggleableInputs).toHaveLength(5); + for (const input of toggleableInputs) { + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute("readonly"); + } + + // additional details are not shown by default + const showMoreButton = screen.queryByTestId("toggle-additional-infos-btn"); + expect(showMoreButton).toBeInTheDocument(); + }); + + test("When show more information button is pressed, then additional details are shown", async () => { + const user = userEvent.setup(); + render(PaperDetailsCard, { + target: document.body, + props: { + loadingPaper: Promise.resolve(createPaper()), + allowEditModeToggle: true, + startInEditMode: false, + showButtonBar: true, + }, + }); + + await waitForComponentLoading(); + + const showMoreButton = screen.queryByTestId("toggle-additional-infos-btn"); + expect(showMoreButton).not.toBeNull(); + expect(showMoreButton).toBeInTheDocument(); + + await user.click(showMoreButton!); + + await waitFor(() => { + const paperDetails = screen.queryAllByTestId("paper-detail"); + expect(paperDetails).toHaveLength(7); + }); + + expect(showMoreButton).toHaveTextContent("Show less information"); + + await user.click(showMoreButton!); + + await waitFor(() => { + const paperDetails = screen.queryAllByTestId("paper-detail"); + expect(paperDetails).toHaveLength(4); + }); + + expect(showMoreButton).toHaveTextContent("Show more information"); + }); + + test("When edit mode is toggled, then paper details are in edit mode", async () => { + const user = userEvent.setup(); + render(PaperDetailsCard, { + target: document.body, + props: { + loadingPaper: Promise.resolve(createPaper()), + allowEditModeToggle: true, + startInEditMode: false, + showButtonBar: true, + }, + }); + + await waitForComponentLoading(); + + const svgs = document.getElementsByTagName("svg"); + const editButtons: SVGSVGElement[] = []; + for (const svg of svgs) { + if (svg.classList.contains("lucide-pencil")) { + editButtons.push(svg); + } + } + expect(editButtons.length).toBe(2); + + const generalInfoBtn = editButtons[0]; + const abstractBtn = editButtons[1]; + + await user.click(generalInfoBtn); + + await waitFor(() => { + const toggleableInputs = screen.queryAllByTestId("toggleable-input"); + expect(toggleableInputs).toHaveLength(5); + for (const input of toggleableInputs.slice(0, 4)) { + expect(input).toBeInTheDocument(); + expect(input).not.toHaveAttribute("readonly"); + } + expect(toggleableInputs[4]).toBeInTheDocument(); + expect(toggleableInputs[4]).toHaveAttribute("readonly"); + }); + + await user.click(abstractBtn); + + await waitFor(() => { + const toggleableInputs = screen.queryAllByTestId("toggleable-input"); + expect(toggleableInputs).toHaveLength(5); + for (const input of toggleableInputs) { + expect(input).toBeInTheDocument(); + expect(input).not.toHaveAttribute("readonly"); + } + }); + + await user.click(generalInfoBtn); + + await waitFor(() => { + const toggleableInputs = screen.queryAllByTestId("toggleable-input"); + expect(toggleableInputs).toHaveLength(5); + for (const input of toggleableInputs.slice(0, 4)) { + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute("readonly"); + } + expect(toggleableInputs[4]).toBeInTheDocument(); + expect(toggleableInputs[4]).not.toHaveAttribute("readonly"); + }); + + await user.click(abstractBtn); + + await waitFor(() => { + const toggleableInputs = screen.queryAllByTestId("toggleable-input"); + expect(toggleableInputs).toHaveLength(5); + for (const input of toggleableInputs) { + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute("readonly"); + } + }); + }); + + test("When editMode is not allowed, then edit buttons are not shown", async () => { + render(PaperDetailsCard, { + target: document.body, + props: { + loadingPaper: Promise.resolve(createPaper()), + allowEditModeToggle: false, + startInEditMode: false, + showButtonBar: true, + }, + }); + + await waitForComponentLoading(); + + const editButtons = document.getElementsByTagName("svg"); + let editButtonCount = 0; + for (const editButton of editButtons) { + if (!editButton.classList.contains("lucide-pencil")) { + continue; + } + + expect(editButton).toBeInTheDocument(); + editButtonCount++; + } + expect(editButtonCount).toBe(0); + }); + + test("When paper is loading, then skeletons are shown", async () => { + render(PaperDetailsCard, { + target: document.body, + props: { + loadingPaper: new Promise((resolve) => { + setTimeout(() => { + resolve(createPaper()); + }, 1000); + }), + allowEditModeToggle: true, + startInEditMode: false, + showButtonBar: true, + }, + }); + + const skeletons = screen.queryAllByTestId("skeleton"); + expect(skeletons).toHaveLength(11); + }); +}); diff --git a/tests/integration/paper-components/paper-view/decision-buttons/accept-button.test.ts b/tests/integration/paper-components/paper-view/decision-buttons/accept-button.test.ts index f68d6f09..32f1c971 100644 --- a/tests/integration/paper-components/paper-view/decision-buttons/accept-button.test.ts +++ b/tests/integration/paper-components/paper-view/decision-buttons/accept-button.test.ts @@ -7,7 +7,7 @@ describe("AcceptButton", () => { render(AcceptButton, { target: document.body, props: { - paperId: 1, + loadingPaperId: Promise.resolve(1), }, }); diff --git a/tests/integration/paper-components/paper-view/decision-buttons/decline-button.test.ts b/tests/integration/paper-components/paper-view/decision-buttons/decline-button.test.ts index a1975022..b1ec5c38 100644 --- a/tests/integration/paper-components/paper-view/decision-buttons/decline-button.test.ts +++ b/tests/integration/paper-components/paper-view/decision-buttons/decline-button.test.ts @@ -7,7 +7,7 @@ describe("DeclineButton", () => { render(DeclineButton, { target: document.body, props: { - paperId: 1, + loadingPaperId: Promise.resolve(1), }, }); diff --git a/tests/integration/paper-components/paper-view/decision-buttons/maybe-button.test.ts b/tests/integration/paper-components/paper-view/decision-buttons/maybe-button.test.ts index aba26bf4..5fbe0551 100644 --- a/tests/integration/paper-components/paper-view/decision-buttons/maybe-button.test.ts +++ b/tests/integration/paper-components/paper-view/decision-buttons/maybe-button.test.ts @@ -7,7 +7,7 @@ describe("MaybeButton", () => { render(MaybeButton, { target: document.body, props: { - paperId: 1, + loadingPaperId: Promise.resolve(1), }, }); diff --git a/tests/integration/paper-components/paper-view/paper-detail.test.ts b/tests/integration/paper-components/paper-view/paper-detail.test.ts new file mode 100644 index 00000000..ac4328f5 --- /dev/null +++ b/tests/integration/paper-components/paper-view/paper-detail.test.ts @@ -0,0 +1,77 @@ +import PaperDetail from "$lib/components/composites/paper-components/paper-view/PaperDetail.svelte"; +import { render, screen } from "@testing-library/svelte"; +import { describe, expect, test } from "vitest"; +import { createPaper } from "../../../model-builder"; +import type { Paper } from "$lib/model/backend"; +import { waitForComponentLoading } from "../../test-helper"; + +describe("PaperDetail", () => { + test("When props are provided, then component is shown", async () => { + render(PaperDetail, { + target: document.body, + props: { + key: "Title", + value: "Example Title", + loadingPaper: Promise.resolve(createPaper()), + areDetailsInEditMode: false, + }, + }); + + await waitForComponentLoading(); + + const spans = document.getElementsByTagName("span"); + expect(spans).toHaveLength(1); + const keySpan = spans[0]; + expect(keySpan.textContent).toEqual("Title"); + + const textareas = document.getElementsByTagName("textarea"); + expect(textareas).toHaveLength(1); + const input = textareas[0]; + expect(input.value).toEqual("Example Title"); + }); + + test("When paper is loading, then skeleton is shown", () => { + render(PaperDetail, { + target: document.body, + props: { + key: "Title", + value: "Example Title", + loadingPaper: new Promise((resolve) => { + setTimeout(() => { + resolve(createPaper()); + }, 1000); + }), + areDetailsInEditMode: false, + }, + }); + + const spans = document.getElementsByTagName("span"); + expect(spans.length).toEqual(1); + const keySpan = spans[0]; + + expect(keySpan.textContent).toEqual("Title"); + expect(screen.queryByTestId("skeleton")).not.toBeNull(); + }); + + test("When paper loading failed, then error text is shown", async () => { + render(PaperDetail, { + target: document.body, + props: { + key: "Title", + value: "Example Title", + loadingPaper: Promise.reject(), + areDetailsInEditMode: false, + }, + }); + + await waitForComponentLoading(); + + const spans = document.getElementsByTagName("span"); + expect(spans).toHaveLength(2); + const keySpan = spans[0]; + const valueSpan = spans[1]; + + expect(keySpan.textContent).toEqual("Title"); + expect(valueSpan.textContent).toEqual("Couldn't load Title"); + }); +}); diff --git a/tests/integration/paper-components/paper-view/paper-view.test.ts b/tests/integration/paper-components/paper-view/paper-view.test.ts index 557e127d..2d18c5b9 100644 --- a/tests/integration/paper-components/paper-view/paper-view.test.ts +++ b/tests/integration/paper-components/paper-view/paper-view.test.ts @@ -1,13 +1,13 @@ import { render, screen } from "@testing-library/svelte"; import { assert, describe, expect, test } from "vitest"; import PaperView from "$lib/components/composites/paper-components/paper-view/PaperView.svelte"; -import { createPaper, createUser } from "../../../model-builder"; +import { createLoadingPaper, createUser } from "../../../model-builder"; describe("PaperView", () => { test("When `showButtonBar` is false, then button bar isn't shown", () => { render(PaperView, { user: createUser(), - paper: createPaper(), + loadingPaper: createLoadingPaper(), showButtonBar: false, backRef: "", userConfig: { @@ -22,7 +22,7 @@ describe("PaperView", () => { test("When `showButtonBar` is true, then navigation buttons are shown", () => { render(PaperView, { user: createUser(), - paper: createPaper(), + loadingPaper: createLoadingPaper(), showButtonBar: true, backRef: "", userConfig: { @@ -38,7 +38,7 @@ describe("PaperView", () => { test("When navigation buttons are shown and `isReviewMode` is true, then the decision buttons are shown", () => { render(PaperView, { user: createUser(), - paper: createPaper(), + loadingPaper: createLoadingPaper(), showButtonBar: true, backRef: "", userConfig: { @@ -57,7 +57,7 @@ describe("PaperView", () => { test("When navigation and decision buttons are shown but `showMaybeButton` is false, then only the accept and decline buttons are shown", () => { render(PaperView, { user: createUser(), - paper: createPaper(), + loadingPaper: createLoadingPaper(), showButtonBar: true, backRef: "", userConfig: { diff --git a/tests/integration/test-helper.ts b/tests/integration/test-helper.ts new file mode 100644 index 00000000..403a6f2e --- /dev/null +++ b/tests/integration/test-helper.ts @@ -0,0 +1,9 @@ +import { waitFor, screen } from "@testing-library/svelte"; +import { expect } from "vitest"; + +export function waitForComponentLoading(): Promise { + return waitFor(() => { + const skeletons = screen.queryAllByTestId("skeleton"); + expect(skeletons).toHaveLength(0); + }); +} diff --git a/tests/model-builder.ts b/tests/model-builder.ts index f2374129..df3d200e 100644 --- a/tests/model-builder.ts +++ b/tests/model-builder.ts @@ -102,3 +102,11 @@ export function createPaper(paper: Partial = {}): Paper { ...paper, }; } + +export function createLoadingPaper(paper: Partial = {}): Promise { + return Promise.resolve(createPaper(paper)); +} + +export function createLoadingProject(project: Partial = {}): Promise { + return Promise.resolve(createProject(project)); +}