diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index adef77fe..37a639c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,9 @@ jobs: python -m pip install --upgrade pip pip install -r tests/requirements/${{ matrix.requirements-file }} python setup.py install + npm install + npx webpack + playwright install --with-deps - name: Run coverage run: coverage run -m pytest diff --git a/private/js/cms.editor.js b/private/js/cms.editor.js index aa05f629..71cd048d 100644 --- a/private/js/cms.editor.js +++ b/private/js/cms.editor.js @@ -1,5 +1,5 @@ -/* eslint-env es6 */ -/* jshint esversion: 6 */ +/* eslint-env es11 */ +/* jshint esversion: 11 */ /* global window, document, fetch, IntersectionObserver, URLSearchParams, console */ import CmsTextEditor from './cms.texteditor.js'; @@ -90,7 +90,7 @@ class CMSEditor { // Add event listener to delete data on modal cancel if (settings.revert_on_cancel) { const CMS = this.CMS; - const csrf = CMS.config.csrf; + const csrf = CMS.config?.csrf || document.querySelector('input[name="csrfmiddlewaretoken"]').value; CMS.API.Helpers.addEventListener( 'modal-close.text-plugin.text-plugin-' + settings.plugin_id, function(e, opts) { diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 00000000..9a669343 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,61 @@ +import os +import pytest +from pytest_django.live_server_helper import LiveServer +from playwright.sync_api import sync_playwright + +from tests.fixtures import DJANGO_CMS4 + + +@pytest.fixture(scope="session") +def live_server(): + server = LiveServer("127.0.0.1:9090") + yield server + server.stop() + + +@pytest.fixture(scope="session") +def browser_context(): + with sync_playwright() as p: + browser = p.chromium.launch() + context = browser.new_context() + yield context + context.close() + browser.close() + + +@pytest.fixture(scope="session") +def page(browser_context): + page = browser_context.new_page() + yield page + page.close() + + +@pytest.fixture +def user(db): + from django.contrib.auth import get_user_model + + User = get_user_model() + return User.objects.create_user(username="admin", password="admin", is_staff=True, is_superuser=True) + + +@pytest.fixture +def cms_page(db, user): + from cms.api import create_page + + return create_page("Test Page", "page.html", "en", created_by=user) + + +@pytest.fixture +def text_plugin(db, cms_page): + if not DJANGO_CMS4: + return None + + from cms.api import add_plugin + + page_content = cms_page.pagecontent_set(manager="admin_manager").current_content(language="en").first() + placeholder = page_content.get_placeholders().first() + return add_plugin(placeholder, "TextPlugin", "en", body="

Test content

") + + +def pytest_configure(): + os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" diff --git a/tests/integration/test_text_editor.py b/tests/integration/test_text_editor.py new file mode 100644 index 00000000..e30eb4ef --- /dev/null +++ b/tests/integration/test_text_editor.py @@ -0,0 +1,67 @@ +from unittest import skipIf + +import pytest +from cms.utils.urlutils import admin_reverse +from playwright.sync_api import expect + +from tests.fixtures import DJANGO_CMS4 + + +def login(page, live_server): + page.goto(f"{live_server.url}/en/admin/") + page.fill("input[name='username']", "admin") + page.fill("input[name='password']", "admin") + page.click("input[type='submit']") + # Ensure success + expect(page.locator("h1", has_text="Site administration")).to_be_visible() + + +def get_pagecontent(cms_page): + return cms_page.pagecontent_set(manager="admin_manager").current_content(language="en").first() + + +@pytest.mark.django_db +@skipIf(not DJANGO_CMS4, reason="Integration tests only work on Django CMS 4") +def test_editor_loads(live_server, page, text_plugin): + """Test that tiptap editor loads and initializes properly""" + # Navigate to the text plugin add view + login(page, live_server) + + page.goto(f"{live_server.url}{admin_reverse('cms_placeholder_edit_plugin', args=(text_plugin.pk,))}") + + editor = page.locator(".cms-editor-inline-wrapper.fixed") + expect(editor).to_be_visible() # Editor + + tiptap = page.locator(".ProseMirror.tiptap") + expect(tiptap).to_be_visible() # Editor + + expect(page.locator('div[role="menubar"]')).to_be_visible() # its menu bar + expect(page.locator('button[title="Bold"]')).to_be_visible() # a button in the menu bar + + assert tiptap.inner_text() == "Test content" + + +@pytest.mark.django_db +@skipIf(not DJANGO_CMS4, reason="Integration tests only work on Django CMS 4") +def _test_text_plugin_saves(live_server, page): + """Test that text plugin content saves correctly""" + # Navigate and login (reusing steps from above) + page.goto(f"{live_server.url}/admin/cms/page/add/") + page.fill("input[name='username']", "admin") + page.fill("input[name='password']", "admin") + page.click("input[type='submit']") + + # Add and fill text plugin + page.click("text=Add plugin") + page.click("text=Text") + + iframe_locator = page.frame_locator(".cke_wysiwyg_frame") + iframe_locator.locator("body").fill("Content to save") + + # Save the plugin + page.click("text=Save") + + # Verify saved content + page.reload() + saved_content = iframe_locator.locator("body").inner_text() + assert saved_content == "Content to save" diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt index e27c327c..d2166fa1 100644 --- a/tests/requirements/base.txt +++ b/tests/requirements/base.txt @@ -12,6 +12,8 @@ flake8 flake8-pyproject pytest pytest-django +playwright>=1.42.0 +pytest-playwright>=0.4.0 # other requirements coverage diff --git a/tests/settings.py b/tests/settings.py index 4dc190d9..27e0fe0a 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -25,6 +25,7 @@ def __getitem__(self, item): "django.contrib.sessions", "django.contrib.admin", "django.contrib.messages", + "django.contrib.staticfiles", "easy_thumbnails", "filer", "cms", diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 4a0df00f..6747d1c3 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -125,8 +125,8 @@ def get_post_request(self, data): def get_plugin_id_from_response(self, response): url = unquote(response.url) # Ideal case, this looks like: - # /en/admin/cms/page/edit-plugin/1/ - return re.findall(r"\d+", url)[0] + # /en/admin/cms/placeholder/add-plugin/...?...&plugin=123 + return re.findall(r"plugin=\d+", url)[0][7:] def test_add_and_edit_plugin(self): """ @@ -632,9 +632,9 @@ def test_render_child_plugin_endpoint(self): # rendered_child_plugin = ( "' + f'alt="Preview Disabled Plugin - {child_plugin.pk}" ' + f'title="Preview Disabled Plugin - {child_plugin.pk}" ' + f'id="{child_plugin.pk}" type="PreviewDisabledPlugin">' "Preview is disabled for this plugin" "" )