diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index e6d7584c..018ee5e7 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -30,4 +30,4 @@ jobs: run: pip install semgrep - name: Run Semgrep rules - run: semgrep ci --config ./frappe-semgrep-rules/rules + run: semgrep ci --config ./frappe-semgrep-rules/rules \ No newline at end of file diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js index 2be9a71c..142b2f13 100644 --- a/cypress/e2e/course_creation.cy.js +++ b/cypress/e2e/course_creation.cy.js @@ -1,133 +1,150 @@ describe("Course Creation", () => { it("creates a new course", () => { cy.login(); - cy.visit("/courses"); + cy.wait(1000); + + cy.visit("/lms/courses"); + // Create a course - cy.get("a.btn").contains("Create a Course").click(); - cy.wait(1000); - cy.url().should("include", "/courses/new-course/edit"); - cy.get("#title").type("Test Course"); - cy.get("#intro").type("Test Course Short Introduction"); - cy.get("#description").type("Test Course Description"); - cy.get("#video-link").type("-LPmw2Znl2c"); - cy.get("#tags-input").type("Test"); - cy.get("#published").check(); + cy.get("a").contains("New Course").click(); cy.wait(1000); + cy.url().should("include", "/courses/new/edit"); + + cy.get("label").contains("Title").type("Test Course"); + cy.get("label") + .contains("Short Introduction") + .type("Test Course Short Introduction to test the UI"); + cy.get("div[contenteditable=true").invoke( + "text", + "Test Course 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." + ); + + cy.fixture("profile.png", "base64").then((fileContent) => { + cy.get('input[type="file"]').attachFile({ + fileContent, + fileName: "profile.png", + mimeType: "image/png", + encoding: "base64", + }); + }); + + cy.get("label") + .contains("Preview Video") + .type("https://www.youtube.com/embed/-LPmw2Znl2c"); + cy.get("[id=tags]").type("Learning{enter}Frappe{enter}ERPNext{enter}"); + cy.get("label").contains("Published").click(); + cy.get("label").contains("Published On").type("2021-01-01"); cy.button("Save").click(); // Add Chapter cy.wait(1000); - cy.link("Course Outline").click(); + cy.button("Add Chapter").click(); cy.wait(1000); - cy.get(".edit-header .btn-add-chapter").click(); - cy.wait(500); - cy.get("#chapter-title").type("Test Chapter"); - cy.get("#chapter-description").type("Test Chapter Description"); - cy.button("Save").click(); + cy.get("[id^=headlessui-dialog-panel-") + .should("be.visible") + .within(() => { + cy.get("label").contains("Title").type("Test Chapter"); + cy.button("Add Chapter").click(); + }); // Add Lesson cy.wait(1000); - cy.link("Add Lesson").click(); + cy.button("Add Lesson").click(); + cy.wait(1000); + cy.url().should("include", "/learn/1-1/edit"); cy.wait(1000); - cy.get("#lesson-title").type("Test Lesson"); - // Content - cy.get(".collapse-section.collapsed:first").click(); - cy.get("#lesson-content .ce-block") + cy.get("label").contains("Title").type("Test Lesson"); + /* cy.get("#content .ce-block") .click() - .type( - "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. 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. {enter}" - ); - cy.get("#lesson-content .ce-toolbar__plus").click(); - cy.get('#lesson-content [data-item-name="youtube"]').click(); - cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto"); - cy.button("Insert").click(); - cy.wait(1000); + .invoke("text", "https://www.youtube.com/watch?v=GoDtyItReto"); */ + /* cy.get("#content .ce-block") + .click() + .paste("https://www.youtube.com/watch?v=GoDtyItReto"); */ + + cy.fixture("Youtube.mov", "base64").then((fileContent) => { + cy.get('input[type="file"]').attachFile({ + fileContent, + fileName: "Youtube.mov", + mimeType: "image/png", + encoding: "base64", + }); + }); + cy.get("#content .ce-block").type( + "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. 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." + ); cy.button("Save").click(); // View Course cy.wait(1000); - cy.visit("/courses"); - cy.get(".course-card-title:first").contains("Test Course"); - cy.get(".course-card:first").click(); - cy.url().should("include", "/courses/test-course"); - cy.get("#title").contains("Test Course"); - cy.get(".preview-video").should( + cy.visit("/lms"); + cy.wait(500); + cy.url().should("include", "/lms/courses"); + cy.get(".grid a:first").within(() => { + cy.get("div").contains("Test Course"); + cy.get("div").contains( + "Test Course Short Introduction to test the UI" + ); + cy.get(".course-image") + .invoke("css", "background-image") + .should("include", "/files/profile"); + }); + cy.get(".grid a:first").click(); + cy.url().should("include", "/lms/courses/test-course"); + cy.get("div").contains("Test Course"); + cy.get("div").contains("Test Course Short Introduction to test the UI"); + cy.get("div").contains("Learning"); + cy.get("div").contains("Frappe"); + cy.get("div").contains("ERPNext"); + cy.get("iframe").should( "have.attr", "src", "https://www.youtube.com/embed/-LPmw2Znl2c" ); - cy.get("#intro").contains("Test Course Short Introduction"); // View Chapter - cy.get(".chapter-title-main:first").contains("Test Chapter"); - cy.get(".chapter-description:first").contains( - "Test Chapter Description" - ); - cy.get(".lesson-info:first").contains("Test Lesson"); - cy.get(".lesson-info:first").click(); + cy.get("div").contains("Test Chapter"); + cy.get("[id^=headlessui-disclosure-panel-").within(() => { + cy.get("div").contains("Test Lesson").click(); + }); + cy.wait(1000); // View Lesson - cy.wait(1000); - cy.url().should("include", "learn/1.1"); - cy.get("#title").contains("Test Lesson"); - cy.get(".lesson-video iframe").should( - "have.attr", - "src", - "https://www.youtube.com/embed/GoDtyItReto" - ); - cy.get(".lesson-content-card").contains( - "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. 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." + cy.url().should("include", "/learn/1-1"); + cy.get("div").contains("Test Lesson"); + cy.get("div").contains( + "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. 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. " ); + cy.get("video") + .should("be.visible") + .children("source") + .invoke("attr", "src") + .should("include", "/files/Youtube"); + // Add Discussion - cy.get(".reply").click(); + cy.button("New Question").click(); cy.wait(500); - cy.get(".discussion-modal").should("be.visible"); - - // Enter title - cy.get(".modal .topic-title") - .type("Discussion from tests") - .should("have.value", "Discussion from tests"); - - // Enter comment - cy.get(".modal .discussions-comment").type( - "This is a discussion from the cypress ui tests." - ); - - // Submit - cy.get(".modal .submit-discussion").click(); - cy.wait(2000); - - // Check if discussion is added to page and content is visible - cy.get(".sidebar-parent:first .discussion-topic-title").should( - "have.text", - "Discussion from tests" - ); - cy.get(".sidebar-parent:first .discussion-topic-title").click(); - cy.get(".discussion-on-page:visible").should("have.class", "show"); - cy.get( - ".discussion-on-page:visible .reply-card .reply-text .ql-editor p" - ).should( - "have.text", - "This is a discussion from the cypress ui tests." - ); - - cy.get(".discussion-form:visible .discussions-comment").type( - "This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page." - ); - - cy.get(".discussion-form:visible .submit-discussion").click(); - cy.wait(3000); - cy.get(".discussion-on-page:visible").should("have.class", "show"); - cy.get(".discussion-on-page:visible") - .children(".reply-card") - .eq(1) - .find(".reply-text") - .should( - "have.text", - "This is a discussion from the cypress ui tests. This comment was entered through the commentbox on the page.\n" + cy.get("[id^=headlessui-dialog-panel-").within(() => { + cy.get("label").contains("Title").type("Test Discussion"); + cy.get("div[contenteditable=true]").invoke( + "text", + "This is a test discussion. This will check if the UI is working properly." ); + cy.button("Post").click(); + }); + + // View Discussion + cy.wait(500); + cy.get("div").contains("Test Discussion").click(); + cy.get("div[contenteditable=true").invoke( + "text", + "This is a test comment. This will check if the UI is working properly." + ); + + cy.get("div").contains( + "This is a test comment. This will check if the UI is working properly." + ); }); }); diff --git a/cypress/fixtures/Youtube.mov b/cypress/fixtures/Youtube.mov new file mode 100644 index 00000000..ca6b75a2 Binary files /dev/null and b/cypress/fixtures/Youtube.mov differ diff --git a/cypress/fixtures/profile.png b/cypress/fixtures/profile.png new file mode 100644 index 00000000..1b42bb97 Binary files /dev/null and b/cypress/fixtures/profile.png differ diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 336a6355..ed5c7968 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -24,6 +24,8 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +import "cypress-file-upload"; + Cypress.Commands.add("login", (email, password) => { if (!email) { email = Cypress.config("testUser") || "Administrator"; @@ -53,3 +55,13 @@ Cypress.Commands.add("iconButton", (text) => { Cypress.Commands.add("dialog", (selector) => { return cy.get(`[role=dialog] ${selector}`); }); + +Cypress.Commands.add("paste", { prevSubject: true }, (subject, text) => { + cy.wrap(subject).then(($element) => { + const element = $element[0]; + element.focus(); + element.textContent = text; + const event = new Event("paste", { bubbles: true }); + element.dispatchEvent(event); + }); +}); diff --git a/frappe-ui b/frappe-ui index c5faaae3..38728b80 160000 --- a/frappe-ui +++ b/frappe-ui @@ -1 +1 @@ -Subproject commit c5faaae38ec6314879aa0abf3a3f992cb6f2240b +Subproject commit 38728b80aaf0cf0a74b2f10e778363c9308c3a1e diff --git a/frontend/package.json b/frontend/package.json index 6489fd75..5f6f4516 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,7 @@ "chart.js": "^4.4.1", "dayjs": "^1.11.6", "feather-icons": "^4.28.0", - "frappe-ui": "^0.1.50", + "frappe-ui": "^0.1.56", "lucide-vue-next": "^0.309.0", "markdown-it": "^14.0.0", "pinia": "^2.0.33", diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 8548333c..cb22560f 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -10,7 +10,7 @@
{ + socket.on('publish_lms_notifications', (data) => { + unreadNotifications.reload() + }) +}) + +const unreadNotifications = createResource({ + cache: 'Unread Notifications Count', + url: 'frappe.client.get_count', + makeParams(values) { + return { + doctype: 'Notification Log', + filters: { + for_user: user, + read: 0, + }, + } + }, + onSuccess(data) { + unreadCount.value = data + }, + auto: true, +}) + +const sidebarLinks = computed(() => { + const links = getSidebarLinks() + if (user) { + links.push({ + label: 'Notifications', + icon: Bell, + to: 'Notifications', + activeFor: ['Notifications'], + count: unreadCount.value, + }) + } + return links +}) const getSidebarFromStorage = () => { return useStorage('sidebar_is_collapsed', false) diff --git a/frontend/src/components/BatchCourses.vue b/frontend/src/components/BatchCourses.vue index 241c5ae9..da31db76 100644 --- a/frontend/src/components/BatchCourses.vue +++ b/frontend/src/components/BatchCourses.vue @@ -19,8 +19,14 @@