diff --git a/cypress.config.ts b/cypress.config.ts index 6495600c..fc4310cc 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -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) { diff --git a/cypress/e2e/plan-sharing/sharingOfPlan.cy.ts b/cypress/e2e/plan-sharing/sharingOfPlan.cy.ts new file mode 100644 index 00000000..1c51cdd2 --- /dev/null +++ b/cypress/e2e/plan-sharing/sharingOfPlan.cy.ts @@ -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 }); + }); +}) diff --git a/cypress/e2e/plans/plans.cy.ts b/cypress/e2e/plans/plans.cy.ts index fe60ff5f..c8763dcb 100644 --- a/cypress/e2e/plans/plans.cy.ts +++ b/cypress/e2e/plans/plans.cy.ts @@ -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' }); @@ -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 }); }); }); } @@ -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'); @@ -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'); - + }); }) diff --git a/package-lock.json b/package-lock.json index c970f158..06d90846 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "@vue/tsconfig": "^0.4.0", "autoprefixer": "^10.4.17", "cypress": "^13.14.2", - "cypress-real-events": "^1.14.0", "eslint": "^8.44.0", "eslint-plugin-vue": "^9.15.1", "typescript": "^5.1.6", @@ -4624,16 +4623,6 @@ "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, - "node_modules/cypress-real-events": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/cypress-real-events/-/cypress-real-events-1.14.0.tgz", - "integrity": "sha512-XmI8y3OZLh6cjRroPalzzS++iv+pGCaD9G9kfIbtspgv7GVsDt30dkZvSXfgZb4rAN+3pOkMVB7e0j4oXydW7Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "cypress": "^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x || ^14.x" - } - }, "node_modules/cypress/node_modules/commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", diff --git a/src/api/plan.ts b/src/api/plan.ts index 76821899..9d7b120f 100644 --- a/src/api/plan.ts +++ b/src/api/plan.ts @@ -60,4 +60,19 @@ const bookmarkPlan = async (planId: string, token: string): Promise => { } } -export { fetchSavedPlans, savePlan, deletePlan, bookmarkPlan }; +const fetchSharedPlan = async (slug: string|string[]): Promise => { + 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 }; diff --git a/src/components/SavedPlans.vue b/src/components/SavedPlans.vue index 470ba223..1a0264d0 100644 --- a/src/components/SavedPlans.vue +++ b/src/components/SavedPlans.vue @@ -37,13 +37,10 @@ > {{ plan.name }} - +
{{ plan.name }} - + + +
+ + + +
+ +
+ + + +
+
+ + + +
+
+
+ + + diff --git a/src/components/__tests__/SavedPlanActionMenu.cy.ts b/src/components/__tests__/SavedPlanActionMenu.cy.ts new file mode 100644 index 00000000..aeba61f3 --- /dev/null +++ b/src/components/__tests__/SavedPlanActionMenu.cy.ts @@ -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'); + }); +}); diff --git a/src/helpers/plan-helper.ts b/src/helpers/plan-helper.ts index 64906a41..4413bded 100644 --- a/src/helpers/plan-helper.ts +++ b/src/helpers/plan-helper.ts @@ -7,6 +7,7 @@ export function map(dto: PlanDTO): Plan { name: dto.name, content: dto.content, bookmark: dto.bookmark, + publicSlug: dto.public_slug, createdAt: dto.created_at, userId: dto.user_id, }; diff --git a/src/router/index.ts b/src/router/index.ts index c9fc7104..f9849605 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,12 +1,18 @@ import { createRouter, createWebHashHistory } from 'vue-router'; import Home from '../views/Home.vue'; +import SharingRedirect from "../views/SharingRedirect.vue"; const routes = [ + { + path: '/shared/:slug', + name: 'SharingRedirect', + component: SharingRedirect + }, { path: '/:catchAll(.*)', name: 'Home', component: Home, - }, + } ]; const router = createRouter({ diff --git a/src/types/Plan.ts b/src/types/Plan.ts index fbf3e3bf..9cd75b1f 100644 --- a/src/types/Plan.ts +++ b/src/types/Plan.ts @@ -3,6 +3,7 @@ export interface Plan { name: string; content: string; bookmark: boolean; + publicSlug: string; createdAt: string; userId: string; } diff --git a/src/types/PlanDTO.ts b/src/types/PlanDTO.ts index 9d33060c..6baddd08 100644 --- a/src/types/PlanDTO.ts +++ b/src/types/PlanDTO.ts @@ -3,6 +3,7 @@ export interface PlanDTO { name: string; content: string; bookmark: boolean; + public_slug: string; created_at: string; user_id: string; } diff --git a/src/views/SharingRedirect.vue b/src/views/SharingRedirect.vue new file mode 100644 index 00000000..5ce4e5c6 --- /dev/null +++ b/src/views/SharingRedirect.vue @@ -0,0 +1,14 @@ + + +