Skip to content
This repository was archived by the owner on Mar 18, 2026. It is now read-only.
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
1 change: 1 addition & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { clerkSetup } from '@clerk/testing/cypress'
import { defineConfig } from "cypress";

export default defineConfig({
defaultCommandTimeout: 15000,
e2e: {
baseUrl: "http://localhost:5173",
setupNodeEvents(on, config) {
Expand Down
56 changes: 56 additions & 0 deletions cypress/e2e/plan-sharing/sharingOfPlan.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { setupClerkTestingToken } from "@clerk/testing/cypress";

describe('sharing of plans', () => {
it('share and open plan', () => {
const expectedModules = [
"Computer Grafik"
]

setupClerkTestingToken();
cy.visit('/',{
onBeforeLoad(win) {
if (!win.navigator.clipboard) {
Object.defineProperty(win.navigator, 'clipboard', {
value: {
writeText: cy.stub().as('writeTextStub').resolves()
},
writable: false
});
} else {
cy.stub(win.navigator.clipboard, 'writeText').as('writeTextStub').resolves();
}
}
});

cy.clerkSignIn({ strategy: 'email_code', identifier: 'user1+clerk_test@lost.university' });

cy.get('[data-cy="ModuleSearch-OpenButton"]').first().click();
cy.get('[data-cy="ModuleSearch-Input"]').type(expectedModules[0]);
cy.get('[data-cy="ModuleSearch-ModuleList"]').first().click();

cy.get('[data-cy="SavedPlans-Dropdown-Button"]').first().click();
cy.get('[data-cy="SavePlan-Button"]').first().click();
cy.get('[data-cy="SavePlan-Name"]').first().type("share plan");
cy.get('[data-cy="SavePlan-Submit"]').first().click();

cy.get('[data-cy=SavedPlansActionMenu-Menu-Button]').first().click();

cy.get('[data-cy="semester-container"]').screenshot('sharingOfPlan.cy.ts/show-action-menu-options');

cy.get('[data-cy=SavedPlansActionMenu-Share-Button]').first().click();

cy.get('@writeTextStub')
.should('have.been.calledOnce')
.then((writeText: any) => {
const copiedUrl = writeText.getCall(0).args[0];
const relativeUrl = copiedUrl.substring(copiedUrl.indexOf('/#'));
cy.visit(relativeUrl);
});

cy.get('[data-cy="module-name"]').should('have.length', expectedModules.length);

cy.get('[data-cy="semester-container"]').screenshot('sharingOfPlan.cy.ts/open-shared-plan');

cy.get('[data-cy=SavedPlansActionMenu-Delete-Button]').click({ force: true });
});
})
13 changes: 7 additions & 6 deletions cypress/e2e/plans/plans.cy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setupClerkTestingToken } from "@clerk/testing/cypress";

describe('multiple plans', () => {
beforeEach(() => {
beforeEach(() => {
setupClerkTestingToken();
cy.visit('/');
cy.clerkSignIn({ strategy: 'email_code', identifier: 'user1+clerk_test@lost.university' });
Expand All @@ -12,7 +12,8 @@ describe('multiple plans', () => {
if ($body.find('[data-cy=SavedPlans-List-Item]').length > 0) {
cy.get('[data-cy=SavedPlans-List-Item]').each(($el) => {
cy.wrap($el).within(() => {
cy.get('[data-cy=SavedPlans-Delete-Button]').click({ force: true });
cy.get('[data-cy=SavedPlansActionMenu-Menu-Button]').click({ force: true });
cy.get('[data-cy=SavedPlansActionMenu-Delete-Button]').click({ force: true });
});
});
}
Expand Down Expand Up @@ -81,8 +82,8 @@ describe('multiple plans', () => {

cy.get("[data-cy=SavedPlans-List-Item").filter(`:contains("Test Plan: To Delete")`)
.first().within(() => {
cy.get('[data-cy=SavedPlans-Delete-Button]')
.first().click()
cy.get('[data-cy=SavedPlansActionMenu-Menu-Button]').first().click()
cy.get('[data-cy=SavedPlansActionMenu-Delete-Button]').first().click()
});

cy.contains('[data-cy="SavedPlans-List-Item"]', "Test Plan: To Delete").should('not.exist');
Expand Down Expand Up @@ -110,9 +111,9 @@ describe('multiple plans', () => {
cy.get('[data-cy="SavedPlans-Bookmark-Button"]')
.find('svg')
.should('have.attr', 'data-prefix', 'fas');
});;
});

cy.get('[data-cy="semester-container"]').screenshot('plans.cy.ts/plan-bookmarked');

});
})
11 changes: 0 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion src/api/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,19 @@ const bookmarkPlan = async (planId: string, token: string): Promise<void> => {
}
}

export { fetchSavedPlans, savePlan, deletePlan, bookmarkPlan };
const fetchSharedPlan = async (slug: string|string[]): Promise<string> => {
try {
const response = await fetch(`/api/plans/shared/${slug}`);
const data = await response.json();
if (data.content) {
return `/plan/${data.content}`
} else {
return `/`
}
} catch (err) {
console.error('Sharing link error:', err);
return `/`
}
}

export { fetchSavedPlans, savePlan, deletePlan, bookmarkPlan, fetchSharedPlan };
25 changes: 11 additions & 14 deletions src/components/SavedPlans.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,10 @@
>
{{ plan.name }}
</router-link>
<button
class="p-2 hover:bg-gray-100 dark:bg-zinc-800 dark:hover:bg-zinc-700 rounded-sm"
data-cy="SavedPlans-Delete-Button"
@click="deletePlan(plan.id)"
>
Löschen
</button>
<SavedPlansActionMenu
:plan="plan"
@delete="deletePlan"
/>
</li>
</ul>
<form
Expand Down Expand Up @@ -107,13 +104,11 @@
>
{{ plan.name }}
</router-link>
<button
class="p-2 hover:bg-gray-100 dark:bg-zinc-800 dark:hover:bg-zinc-700 rounded-sm"
data-cy="SavedPlans-Delete-Button"
@click="deletePlan(plan.id)"
>
Löschen
</button>
<SavedPlansActionMenu
:plan="plan"
:menu-position-class="'right-0 mt-1'"
@delete="deletePlan"
/>
</li>
</ul>
<form
Expand Down Expand Up @@ -153,6 +148,7 @@ import { defineComponent } from 'vue';
import { useAuth } from "@clerk/vue";
import { fetchSavedPlans, savePlan, deletePlan, bookmarkPlan } from "../api/plan";
import type { Plan } from "../types/Plan";
import SavedPlansActionMenu from "./SavedPlansActionMenu.vue";
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue';
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from '@fortawesome/fontawesome-svg-core';
Expand All @@ -163,6 +159,7 @@ library.add(faChevronDown);
export default defineComponent({
name: 'SavedPlans',
components: {
SavedPlansActionMenu,
Popover,
PopoverButton,
PopoverPanel,
Expand Down
116 changes: 116 additions & 0 deletions src/components/SavedPlansActionMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<HeadlessUIMenu
as="div"
class="relative inline-block text-left z-50"
>
<div>
<MenuButton
class="p-2 hover:bg-gray-100 dark:bg-zinc-800 dark:hover:bg-zinc-700 rounded-sm"
data-cy="SavedPlansActionMenu-Menu-Button"
>
<font-awesome-icon
:icon="['fas', 'ellipsis']"
/>
</MenuButton>
</div>
<MenuItems
class="absolute z-10 bg-white dark:bg-zinc-800 shadow-lg rounded-sm"
:class="menuPositionClass"
>
<div class="px-1 py-1">
<MenuItem
v-slot="{ close }"
as="div"
>
<button
class="
flex items-center gap-2 w-full text-left px-4 py-2
hover:bg-gray-100 dark:bg-zinc-800 dark:hover:bg-zinc-700
"
data-cy="SavedPlansActionMenu-Share-Button"
@click.stop.prevent="sharePlan(close)"
>
<font-awesome-icon
data-cy="SavedPlansActionMenu-Share-Icon"
:icon="planCopied ? ['fas', 'check'] : ['fas', 'share-nodes']"
:class="planCopied ? 'text-green-600' : 'text-black dark:text-white'"
/>
</button>
</MenuItem>
</div>
<div class="px-1 py-1">
<MenuItem
as="div"
>
<button
class="
flex items-center gap-2 w-full text-left px-4 py-2
hover:bg-gray-100 dark:bg-zinc-800 dark:hover:bg-zinc-700
"
data-cy="SavedPlansActionMenu-Delete-Button"
@click="deletePlan"
>
<font-awesome-icon :icon="['fas', 'trash']" />
</button>
</MenuItem>
</div>
</MenuItems>
</HeadlessUIMenu>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import type { Plan } from '../types/Plan';
import { Menu as HeadlessUIMenu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue';
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faTrash, faCheck, faEllipsis, faShareNodes } from '@fortawesome/free-solid-svg-icons';
import { library } from "@fortawesome/fontawesome-svg-core";

library.add(faTrash, faCheck, faEllipsis, faShareNodes);

export default defineComponent({
name: 'SavedPlansActionMenu',
components: {
HeadlessUIMenu,
MenuButton,
MenuItems,
MenuItem,
FontAwesomeIcon
},
props: {
plan: {
type: Object as () => Plan,
required: true
},
menuPositionClass: {
type: String,
default: 'top-0 left-full ml-1'
}
},
emits: ['delete'],
data() {
return {
planCopied: false
}
},
methods: {
async deletePlan() {
this.$emit('delete', this.plan.id);
},
async sharePlan(close: () => void) {
const baseUrl = window.location.origin;
const shareUrl = `${baseUrl}/#/shared/${this.plan.publicSlug}`;
try {
await navigator.clipboard.writeText(shareUrl);
this.planCopied = true;
setTimeout(() => {
this.planCopied = false;
close();
}, 1000);
} catch (err) {
console.error('Failed to copy link:', err);
}
},
}
});
</script>
70 changes: 70 additions & 0 deletions src/components/__tests__/SavedPlanActionMenu.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { mount } from 'cypress/vue';

import SavedPlansActionMenu from '../SavedPlansActionMenu.vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
import {
faTrash,
faShareNodes,
faCheck,
faEllipsis
} from '@fortawesome/free-solid-svg-icons';

library.add(faTrash, faShareNodes, faCheck, faEllipsis);

describe('SavedPlan Action Menu Component', () => {
beforeEach(() => {
cy.window().then((win) => {
if (!win.navigator.clipboard) {
Object.defineProperty(win.navigator, 'clipboard', {
value: {
writeText: cy.stub().as('writeTextStub').resolves()
},
writable: false
});
} else {
cy.stub(win.navigator.clipboard, 'writeText').as('writeTextStub').resolves();
}
});

const fakePlan = {
id: 'plan-1',
name: 'Test Plan',
publicSlug: 'abc123',
content: '/plan/1'
};

const onDelete = cy.spy().as('onDelete');

mount(SavedPlansActionMenu, {
props: {
plan: fakePlan,
menuPositionClass: 'top-0 left-0',
onDelete: onDelete
},
global: {
components: {
FontAwesomeIcon
}
}
});
});

it('Test Menu Items', () => {
cy.get('[data-cy=SavedPlansActionMenu-Menu-Button]').click();

cy.get('[data-cy=SavedPlansActionMenu-Share-Button]').click();

cy.get('[data-cy=SavedPlansActionMenu-Share-Icon]')
.should('have.class', 'text-green-600');

cy.get('[data-cy=SavedPlansActionMenu-Menu-Button]').click();

cy.get('[data-cy=SavedPlansActionMenu-Share-Icon]')
.should('have.class', 'text-black');

cy.get('[data-cy=SavedPlansActionMenu-Delete-Button]').click();

cy.get('@onDelete').should('have.been.calledWith', 'plan-1');
});
});
Loading
Loading