diff --git a/cypress/e2e/batch_creation.cy.js b/cypress/e2e/batch_creation.cy.js new file mode 100644 index 00000000..5b62e6e2 --- /dev/null +++ b/cypress/e2e/batch_creation.cy.js @@ -0,0 +1,180 @@ +describe("Batch Creation", () => { + it("creates a new batch", () => { + cy.login(); + cy.wait(500); + cy.visit("/lms/batches"); + cy.closeOnboardingModal(); + + // Open Settings + cy.get("span").contains("Learning").click(); + cy.get("span").contains("Settings").click(); + + // Add a new member + cy.get('[id^="headlessui-dialog-panel-v-"]') + .find("span") + .contains(/^Members$/) + .click(); + cy.get('[id^="headlessui-dialog-panel-v-"]') + .find("button") + .contains("New") + .click(); + + const dateNow = Date.now(); + const randomEmail = `testuser_${dateNow}@example.com`; + const randomName = `Test User ${dateNow}`; + + cy.get("input[placeholder='Email']").type(randomEmail); + cy.get("input[placeholder='First Name']").type(randomName); + cy.get("button").contains("Add").click(); + + // Add evaluator + cy.get('[id^="headlessui-dialog-panel-v-"]') + .find("span") + .contains(/^Evaluators$/) + .click(); + + cy.get('[id^="headlessui-dialog-panel-v-"]') + .find("button") + .contains("New") + .click(); + const randomEvaluator = `evaluator${dateNow}@example.com`; + + cy.get("input[placeholder='Email']").type(randomEvaluator); + cy.get("button").contains("Add").click(); + cy.get("div").contains(randomEvaluator).should("be.visible").click(); + + cy.visit("/lms/batches"); + cy.closeOnboardingModal(); + + // Create a batch + cy.get("button").contains("New").click(); + cy.wait(500); + cy.url().should("include", "/batches/new/edit"); + cy.get("label").contains("Title").type("Test Batch"); + + cy.get("label").contains("Start Date").type("2030-10-01"); + cy.get("label").contains("End Date").type("2030-10-31"); + cy.get("label").contains("Start Time").type("10:00"); + cy.get("label").contains("End Time").type("11:00"); + cy.get("label").contains("Timezone").type("IST"); + cy.get("label").contains("Seat Count").type("10"); + cy.get("label").contains("Published").click(); + + cy.get("label") + .contains("Short Description") + .type("Test Batch Short Description to test the UI"); + cy.get("div[contenteditable=true").invoke( + "text", + "Test Batch Description. I need a very big description to test the UI. This is a very big description. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now." + ); + + /* Instructor */ + cy.get("label") + .contains("Instructors") + .parent() + .within(() => { + cy.get("input").click().type("evaluator"); + cy.get("input") + .invoke("attr", "aria-controls") + .as("instructor_list_id"); + }); + cy.get("@instructor_list_id").then((instructor_list_id) => { + cy.get(`[id^=${instructor_list_id}`) + .should("be.visible") + .within(() => { + cy.get("[id^=headlessui-combobox-option-").first().click(); + }); + }); + + cy.button("Save").click(); + cy.wait(1000); + let batchName; + cy.url().then((url) => { + console.log(url); + batchName = url.split("/").pop(); + cy.wrap(batchName).as("batchName"); + }); + cy.wait(500); + + // View Batch + cy.wait(1000); + cy.visit("/lms/batches"); + cy.closeOnboardingModal(); + + cy.url().should("include", "/lms/batches"); + + cy.get('[id^="headlessui-radiogroup-v-"]') + .find("span") + .contains("Upcoming") + .should("be.visible") + .click(); + + cy.get("@batchName").then((batchName) => { + cy.get(`a[href='/lms/batches/details/${batchName}'`).within(() => { + cy.get("div").contains("Test Batch").should("be.visible"); + cy.get("div") + .contains("Test Batch Short Description to test the UI") + .should("be.visible"); + cy.get("span") + .contains("01 Oct 2030 - 31 Oct 2030") + .should("be.visible"); + cy.get("span") + .contains("10:00 AM - 11:00 AM") + .should("be.visible"); + cy.get("span").contains("IST").should("be.visible"); + cy.get("a").contains("Evaluator").should("be.visible"); + cy.get("div") + .contains("10") + .should("be.visible") + .get("span") + .contains("Seats Left") + .should("be.visible"); + }); + cy.get(`a[href='/lms/batches/details/${batchName}'`).click(); + }); + + cy.get("div").contains("Test Batch").should("be.visible"); + cy.get("div") + .contains("Test Batch Short Description to test the UI") + .should("be.visible"); + cy.get("a").contains("Evaluator").should("be.visible"); + cy.get("span") + .contains("01 Oct 2030 - 31 Oct 2030") + .should("be.visible"); + cy.get("span").contains("10:00 AM - 11:00 AM").should("be.visible"); + cy.get("span").contains("IST").should("be.visible"); + cy.get("div") + .contains("10") + .should("be.visible") + .get("span") + .contains("Seats Left") + .should("be.visible"); + + cy.get("p") + .contains( + "Test Batch Description. I need a very big description to test the UI. This is a very big description. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now." + ) + .should("be.visible"); + cy.get("button").contains("Manage Batch").click(); + + /* Add student to batch */ + cy.get("button").contains("Add").click(); + cy.get('div[id^="headlessui-dialog-panel-v-"]') + .first() + .find("button") + .eq(1) + .click(); + cy.get("input[id^='headlessui-combobox-input-v-']").type(randomEmail); + cy.get("div").contains(randomEmail).click(); + cy.get("button").contains("Submit").click(); + + // Verify Seat Count + cy.get("span").contains("Details").click(); + cy.get("div") + .contains("9") + .should("be.visible") + .get("span") + .contains("Seats Left") + .should("be.visible"); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index d32be479..757d42cd 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -72,8 +72,15 @@ Cypress.Commands.add("paste", { prevSubject: true }, (subject, text) => { Cypress.Commands.add("closeOnboardingModal", () => { cy.wait(500); - cy.get('[class*="z-50"]') - .find('button:has(svg[class*="feather-x"])') - .realClick(); - cy.wait(1000); + cy.get("body").then(($body) => { + // Check if any element with class including 'z-50' exists + if ($body.find('[class*="z-50"]').length > 0) { + cy.get('[class*="z-50"]') + .find('button:has(svg[class*="feather-x"])') + .realClick(); + cy.wait(1000); + } else { + cy.log("Onboarding modal not found, skipping close."); + } + }); }); diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 585aa545..2212eb18 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -27,9 +27,9 @@ declare module 'vue' { BatchOverlay: typeof import('./src/components/BatchOverlay.vue')['default'] BatchStudentProgress: typeof import('./src/components/Modals/BatchStudentProgress.vue')['default'] BatchStudents: typeof import('./src/components/BatchStudents.vue')['default'] - BrandSettings: typeof import('./src/components/BrandSettings.vue')['default'] + BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default'] BulkCertificates: typeof import('./src/components/Modals/BulkCertificates.vue')['default'] - Categories: typeof import('./src/components/Categories.vue')['default'] + Categories: typeof import('./src/components/Settings/Categories.vue')['default'] CertificationLinks: typeof import('./src/components/CertificationLinks.vue')['default'] ChapterModal: typeof import('./src/components/Modals/ChapterModal.vue')['default'] CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default'] @@ -48,10 +48,10 @@ declare module 'vue' { EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default'] EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default'] EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default'] - EmailTemplates: typeof import('./src/components/EmailTemplates.vue')['default'] + EmailTemplates: typeof import('./src/components/Settings/EmailTemplates.vue')['default'] EmptyState: typeof import('./src/components/EmptyState.vue')['default'] EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default'] - Evaluators: typeof import('./src/components/Evaluators.vue')['default'] + Evaluators: typeof import('./src/components/Settings/Evaluators.vue')['default'] Event: typeof import('./src/components/Modals/Event.vue')['default'] ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default'] FeedbackModal: typeof import('./src/components/Modals/FeedbackModal.vue')['default'] @@ -65,28 +65,30 @@ declare module 'vue' { LessonHelp: typeof import('./src/components/LessonHelp.vue')['default'] Link: typeof import('./src/components/Controls/Link.vue')['default'] LiveClass: typeof import('./src/components/LiveClass.vue')['default'] + LiveClassAttendance: typeof import('./src/components/Modals/LiveClassAttendance.vue')['default'] LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default'] LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default'] - Members: typeof import('./src/components/Members.vue')['default'] + Members: typeof import('./src/components/Settings/Members.vue')['default'] MobileLayout: typeof import('./src/components/MobileLayout.vue')['default'] MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default'] NoPermission: typeof import('./src/components/NoPermission.vue')['default'] NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default'] NotPermitted: typeof import('./src/components/NotPermitted.vue')['default'] PageModal: typeof import('./src/components/Modals/PageModal.vue')['default'] - PaymentSettings: typeof import('./src/components/PaymentSettings.vue')['default'] + PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default'] Play: typeof import('./src/components/Icons/Play.vue')['default'] ProgressBar: typeof import('./src/components/ProgressBar.vue')['default'] Question: typeof import('./src/components/Modals/Question.vue')['default'] Quiz: typeof import('./src/components/Quiz.vue')['default'] QuizBlock: typeof import('./src/components/QuizBlock.vue')['default'] + QuizInVideo: typeof import('./src/components/Modals/QuizInVideo.vue')['default'] Rating: typeof import('./src/components/Controls/Rating.vue')['default'] ReviewModal: typeof import('./src/components/Modals/ReviewModal.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] - SettingDetails: typeof import('./src/components/SettingDetails.vue')['default'] - SettingFields: typeof import('./src/components/SettingFields.vue')['default'] - Settings: typeof import('./src/components/Modals/Settings.vue')['default'] + SettingDetails: typeof import('./src/components/Settings/SettingDetails.vue')['default'] + SettingFields: typeof import('./src/components/Settings/SettingFields.vue')['default'] + Settings: typeof import('./src/components/Settings/Settings.vue')['default'] SidebarLink: typeof import('./src/components/SidebarLink.vue')['default'] StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default'] StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default'] @@ -97,5 +99,7 @@ declare module 'vue' { UserAvatar: typeof import('./src/components/UserAvatar.vue')['default'] UserDropdown: typeof import('./src/components/UserDropdown.vue')['default'] VideoBlock: typeof import('./src/components/VideoBlock.vue')['default'] + ZoomAccountModal: typeof import('./src/components/Modals/ZoomAccountModal.vue')['default'] + ZoomSettings: typeof import('./src/components/Settings/ZoomSettings.vue')['default'] } } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a8398d21..e2f19e48 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -45,7 +45,6 @@ const Layout = computed(() => { onUnmounted(() => { noSidebar.value = false - stopSession() }) watch(userResource, () => { diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 682a4447..b72d311a 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -181,7 +181,16 @@ import UserDropdown from '@/components/UserDropdown.vue' import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue' import SidebarLink from '@/components/SidebarLink.vue' -import { ref, onMounted, inject, watch, reactive, markRaw, h } from 'vue' +import { + ref, + onMounted, + inject, + watch, + reactive, + markRaw, + h, + onUnmounted, +} from 'vue' import { getSidebarLinks } from '../utils' import { usersStore } from '@/stores/user' import { sessionStore } from '@/stores/session' @@ -626,4 +635,8 @@ watch(userResource, () => { const redirectToWebsite = () => { window.open('https://frappe.io/learning', '_blank') } + +onUnmounted(() => { + socket.off('publish_lms_notifications') +}) diff --git a/frontend/src/components/BatchCard.vue b/frontend/src/components/BatchCard.vue index 46b3095b..d1c4c175 100644 --- a/frontend/src/components/BatchCard.vue +++ b/frontend/src/components/BatchCard.vue @@ -1,6 +1,6 @@