diff --git a/.github/helper/install.sh b/.github/helper/install.sh new file mode 100644 index 00000000..b5661726 --- /dev/null +++ b/.github/helper/install.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e +cd ~ || exit + +echo "Setting Up Bench..." + +pip install frappe-bench +bench -v init frappe-bench --skip-assets --python "$(which python)" +cd ./frappe-bench || exit + +bench -v setup requirements + +echo "Setting Up LMS App..." +bench get-app lms "${GITHUB_WORKSPACE}" + +echo "Setting Up Sites & Database..." + +mkdir ~/frappe-bench/sites/lms.test +cp "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/lms.test/site_config.json + + +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL character_set_server = 'utf8mb4'"; +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; + +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE DATABASE test_lms"; +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE USER 'test_lms'@'localhost' IDENTIFIED BY 'test_lms'"; +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "GRANT ALL PRIVILEGES ON \`test_lms\`.* TO 'test_lms'@'localhost'"; + +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "FLUSH PRIVILEGES"; + +echo "Setting Up Procfile..." + +sed -i 's/^watch:/# watch:/g' Procfile +sed -i 's/^schedule:/# schedule:/g' Procfile + +echo "Starting Bench..." + +bench start &> bench_start.log & + +CI=Yes bench build & +build_pid=$! + +bench --site lms.test reinstall --yes +bench --site lms.test install-app lms + +wait $build_pid diff --git a/.github/helper/install_dependencies.sh b/.github/helper/install_dependencies.sh new file mode 100644 index 00000000..b8da0226 --- /dev/null +++ b/.github/helper/install_dependencies.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +echo "Setting Up System Dependencies..." + +sudo apt update +sudo apt install libcups2-dev redis-server mariadb-client-10.6 + +install_wkhtmltopdf() { + wget -q https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb + sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb +} +install_wkhtmltopdf & diff --git a/.github/helper/site_config.json b/.github/helper/site_config.json new file mode 100644 index 00000000..85c9ce00 --- /dev/null +++ b/.github/helper/site_config.json @@ -0,0 +1,20 @@ +{ + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "test_lms", + "db_password": "test_lms", + "allow_tests": true, + "enable_ui_tests": true, + "db_type": "mariadb", + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "root", + "root_password": "123", + "host_name": "http://lms.test:8000", + "monitor": 1, + "server_script_enabled": true, + "mute_emails": true +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18490980..630341d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Run tests +name: Server Tests on: push: branches: diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 00000000..0d0d96bc --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,116 @@ +name: UI + +on: + pull_request: + workflow_dispatch: + push: + branches: [ main ] + +permissions: + # Do not change this as GITHUB_TOKEN is being used by roulette + contents: read + +jobs: + + test: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'frappe' }} + timeout-minutes: 60 + + strategy: + fail-fast: false + + name: UI Tests (Cypress) + + services: + mariadb: + image: mariadb:10.6 + env: + MARIADB_ROOT_PASSWORD: 123 + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Check for valid Python & Merge Conflicts + run: | + python -m compileall -q -f "${GITHUB_WORKSPACE}" + if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 + fi + + - uses: actions/setup-node@v3 + with: + node-version: 16 + check-latest: true + + - name: Add to Hosts + run: | + echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-ui-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn-ui- + + - name: Cache cypress binary + uses: actions/cache@v3 + with: + path: ~/.cache/Cypress + key: ${{ runner.os }}-cypress + + - name: Install Dependencies + run: | + bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} + AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + TYPE: ui + DB: mariadb + + - name: Site Setup + run: | + cd ~/frappe-bench/ + bench --site lms.test execute frappe.utils.install.complete_setup_wizard + bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user + + - name: UI Tests + run: cd ~/frappe-bench/ && bench --site lms.test run-ui-tests lms --headless + env: + CYPRESS_BASE_URL: http://lms.test:8000 + CYPRESS_RECORD_KEY: 095366ec-7b9f-41bd-aeec-03bb76d627fe + + - name: Stop server and wait for coverage file + run: | + ps -ef | grep "[f]rappe serve" | awk '{print $2}' | xargs kill -s SIGINT + sleep 5 + + - name: Show bench output + if: ${{ always() }} + run: cat ~/frappe-bench/bench_start.log || true \ No newline at end of file diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 00000000..d93eac9e --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,18 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + projectId: "vandxn", + adminPassword: "admin", + testUser: "frappe@example.com", + defaultCommandTimeout: 20000, + pageLoadTimeout: 15000, + video: true, + videoUploadOnPasses: false, + retries: { + runMode: 2, + openMode: 0, + }, + e2e: { + baseUrl: "http://test_site_ui:8000", + }, +}); diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js new file mode 100644 index 00000000..87caa7a7 --- /dev/null +++ b/cypress/e2e/course_creation.cy.js @@ -0,0 +1,97 @@ +describe("Course Creation", () => { + it("creates a new course", () => { + cy.login(); + cy.visit("/courses"); + // Create a course + cy.get("a.btn").contains("Create a Course").click(); + cy.wait(1000); + cy.url().should("include", "/courses/new-course"); + cy.button("Add Tag").click(); + cy.get(".course-card-pills").type("Test"); + cy.get("#title").type("Test Course"); + cy.get("#intro").type("Test Course Short Introduction"); + cy.get("#video-link").type("-LPmw2Znl2c"); + cy.get("#published").check(); + cy.get("#description").type("Test Course Description"); + cy.wait(1000); + cy.button("Save Course Details").click(); + + // Add Chapter + cy.wait(3000); + cy.button("New Chapter").click(); + cy.get(".new-chapter .chapter-title-main").type("Test Chapter"); + cy.get(".new-chapter .chapter-description").type( + "Test Chapter Description" + ); + cy.get(".new-chapter .btn-save-chapter").click(); + + // Add Lesson + cy.wait(3000); + cy.get(".chapter-parent .btn-lesson").click(); + + cy.wait(3000); + cy.get("#title").type("Test Lesson"); + cy.get("#youtube").type("GoDtyItReto"); + cy.get("#body").type("Test Lesson Content"); + cy.wait(1000); + cy.get(".btn-lesson").click(); + + // View Course + cy.wait(3000); + 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( + "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(); + + // View Lesson + cy.wait(3000); + 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("Test Lesson Content"); + + // Add Discussion + cy.get(".reply").click(); + cy.wait(500); + cy.get(".topic-title").type("Question Title"); + cy.get(".comment-field").type( + "Question Content. This is a very long question. It contains more than once sentence. Its meant to be this long as this is a UI test." + ); + cy.get(".submit-discussion").click(); + + // View Discussion + cy.wait(3000); + cy.get(".discussion-topic-title:first").contains("Question Title"); + cy.get(".sidebar-parent:first").click(); + cy.get(".reply-text").contains( + "Question Content. This is a very long question. It contains more than once sentence. Its meant to be this long as this is a UI test." + ); + cy.get(".comment-field:visible").type( + "This is a reply to the previous comment. Its not that long." + ); + cy.get(".submit-discussion:visible").click(); + cy.wait(1000); + cy.get(".reply-text:last p").contains( + "This is a reply to the previous comment. Its not that long." + ); + }); +}); diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 00000000..02e42543 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 00000000..336a6355 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,55 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) + +Cypress.Commands.add("login", (email, password) => { + if (!email) { + email = Cypress.config("testUser") || "Administrator"; + } + if (!password) { + password = Cypress.config("adminPassword"); + } + cy.request({ + url: "/api/method/login", + method: "POST", + body: { usr: email, pwd: password }, + }); +}); + +Cypress.Commands.add("button", (text) => { + return cy.get(`button:contains("${text}")`); +}); + +Cypress.Commands.add("link", (text) => { + return cy.get(`a:contains("${text}")`); +}); + +Cypress.Commands.add("iconButton", (text) => { + return cy.get(`button[aria-label="${text}"]`); +}); + +Cypress.Commands.add("dialog", (selector) => { + return cy.get(`[role=dialog] ${selector}`); +}); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 00000000..3a252243 --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import "./commands"; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/lms/lms/doctype/lms_course/lms_course.json b/lms/lms/doctype/lms_course/lms_course.json index 94629b28..0b6a450c 100644 --- a/lms/lms/doctype/lms_course/lms_course.json +++ b/lms/lms/doctype/lms_course/lms_course.json @@ -53,7 +53,6 @@ "in_list_view": 1, "label": "Title", "reqd": 1, - "unique": 1, "width": "200" }, { @@ -261,7 +260,7 @@ } ], "make_attachments_public": 1, - "modified": "2023-02-23 09:45:54.826324", + "modified": "2023-02-23 09:45:54.826327", "modified_by": "Administrator", "module": "LMS", "name": "LMS Course", diff --git a/package.json b/package.json new file mode 100644 index 00000000..c690a31a --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "frappe_lms", + "version": "1.0.0", + "description": "Easy to use, open-source, Learning Management System", + "scripts": { + "test-local": "cypress open --e2e --browser chrome" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/frappe/lms.git" + }, + "author": "Frappe", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/frappe/lms/issues" + }, + "homepage": "https://github.com/frappe/lms#readme", + "devDependencies": { + "cypress": "^10" + }, + "dependencies": { + "@4tw/cypress-drag-drop": "^2", + "@cypress/code-coverage": "^3", + "@testing-library/cypress": "^8", + "@testing-library/dom": "8.17.1", + "cypress-real-events": "^1.7.6" + } +}