import re from typing import List import pytest from playwright.async_api import Browser, Error, Page from django_components import types from django_components.testing import djc_test from tests.testutils import setup_test_config from tests.e2e.utils import TEST_SERVER_URL, with_playwright setup_test_config( components={"autodiscover": False}, extra_settings={ "ROOT_URLCONF": "tests.test_dependency_manager", }, ) urlpatterns: List = [] async def _create_page_with_dep_manager(browser: Browser) -> Page: page = await browser.new_page() # Load the JS library by opening a page with the script, and then running the script code # E.g. `http://localhost:54017/static/django_components/django_components.min.js` script_url = TEST_SERVER_URL + "/static/django_components/django_components.min.js" await page.goto(script_url) # The page's body is the script code. We load it by executing the code await page.evaluate( """ () => { eval(document.body.textContent); } """ ) # Ensure the body is clear await page.evaluate( """ () => { document.body.innerHTML = ''; document.head.innerHTML = ''; } """ ) return page @djc_test( django_settings={ "STATIC_URL": "static/", } ) class TestDependencyManager: @with_playwright async def test_script_loads(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] # Check the exposed API keys = sorted(await page.evaluate("Object.keys(Components)")) assert keys == ["createComponentsManager", "manager", "unescapeJs"] keys = await page.evaluate("Object.keys(Components.manager)") assert keys == [ "callComponent", "registerComponent", "registerComponentData", "loadJs", "loadCss", "markScriptLoaded", ] await page.close() # Tests for `manager.loadJs()` / `manager.loadCss()` / `manager.markAsLoaded()` @djc_test( django_settings={ "STATIC_URL": "static/", } ) class TestLoadScript: @with_playwright async def test_load_js_scripts(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] # JS code that loads a few dependencies, capturing the HTML after each action test_js: types.js = """() => { const manager = Components.createComponentsManager(); const headBeforeFirstLoad = document.head.innerHTML; // Adds a script the first time manager.loadJs(""); const bodyAfterFirstLoad = document.body.innerHTML; // Does not add it the second time manager.loadJs(""); const bodyAfterSecondLoad = document.body.innerHTML; // Adds different script manager.loadJs(""); const bodyAfterThirdLoad = document.body.innerHTML; const headAfterThirdLoad = document.head.innerHTML; return { bodyAfterFirstLoad, bodyAfterSecondLoad, bodyAfterThirdLoad, headBeforeFirstLoad, headAfterThirdLoad, }; }""" data = await page.evaluate(test_js) assert data["bodyAfterFirstLoad"] == '' assert data["bodyAfterSecondLoad"] == '' assert data["bodyAfterThirdLoad"] == '' assert data["headBeforeFirstLoad"] == data["headAfterThirdLoad"] assert data["headBeforeFirstLoad"] == "" await page.close() @with_playwright async def test_load_css_scripts(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] # JS code that loads a few dependencies, capturing the HTML after each action test_js: types.js = """() => { const manager = Components.createComponentsManager(); const bodyBeforeFirstLoad = document.body.innerHTML; // Adds a script the first time manager.loadCss(""); const headAfterFirstLoad = document.head.innerHTML; // Does not add it the second time manager.loadCss(""); const headAfterSecondLoad = document.head.innerHTML; // Adds different script manager.loadCss(""); const headAfterThirdLoad = document.head.innerHTML; const bodyAfterThirdLoad = document.body.innerHTML; return { headAfterFirstLoad, headAfterSecondLoad, headAfterThirdLoad, bodyBeforeFirstLoad, bodyAfterThirdLoad, }; }""" data = await page.evaluate(test_js) assert data["headAfterFirstLoad"] == '' assert data["headAfterSecondLoad"] == '' assert data["headAfterThirdLoad"] == '' assert data["bodyBeforeFirstLoad"] == data["bodyAfterThirdLoad"] assert data["bodyBeforeFirstLoad"] == "" await page.close() @with_playwright async def test_does_not_load_script_if_marked_as_loaded(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] # JS code that loads a few dependencies, capturing the HTML after each action test_js: types.js = """() => { const manager = Components.createComponentsManager(); // Adds a script the first time manager.markScriptLoaded('css', '/one/two'); manager.markScriptLoaded('js', '/one/three'); manager.loadCss(""); const headAfterFirstLoad = document.head.innerHTML; manager.loadJs(""); const bodyAfterSecondLoad = document.body.innerHTML; return { headAfterFirstLoad, bodyAfterSecondLoad, }; }""" data = await page.evaluate(test_js) assert data["headAfterFirstLoad"] == "" assert data["bodyAfterSecondLoad"] == "" await page.close() # Tests for `manager.registerComponent()` / `registerComponentData()` / `callComponent()` @djc_test( django_settings={ "STATIC_URL": "static/", } ) class TestCallComponent: @with_playwright async def test_calls_component_successfully(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] test_js: types.js = """() => { const manager = Components.createComponentsManager(); const compName = 'my_comp'; const compId = 'c12345'; const inputHash = 'input-abc'; // Pretend that this HTML belongs to our component document.body.insertAdjacentHTML('beforeend', '
abc
'); let captured = null; manager.registerComponent(compName, (data, ctx) => { captured = { ctx, data }; return 123; }); manager.registerComponentData(compName, inputHash, () => { return { hello: 'world' }; }); const result = manager.callComponent(compName, compId, inputHash); // Serialize the HTML elements captured.ctx.els = captured.ctx.els.map((el) => el.outerHTML); return { result, captured, }; }""" data = await page.evaluate(test_js) assert data["result"] == 123 assert data["captured"] == { "data": { "hello": "world", }, "ctx": { "els": ['
abc
'], "id": "c12345", "name": "my_comp", }, } await page.close() @with_playwright async def test_calls_component_successfully_async(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] test_js: types.js = """() => { const manager = Components.createComponentsManager(); const compName = 'my_comp'; const compId = 'c12345'; const inputHash = 'input-abc'; // Pretend that this HTML belongs to our component document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponent(compName, (data, ctx) => { return Promise.resolve(123); }); manager.registerComponentData(compName, inputHash, () => { return { hello: 'world' }; }); // Should be Promise const result = manager.callComponent(compName, compId, inputHash); const isPromise = `${result}` === '[object Promise]'; // Wrap the whole response in Promise, so we can add extra fields return Promise.resolve(result).then((res) => ({ result: res, isPromise, })); }""" data = await page.evaluate(test_js) assert data["result"] == 123 assert data["isPromise"] is True await page.close() @with_playwright async def test_error_in_component_call_do_not_propagate_sync(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] test_js: types.js = """() => { const manager = Components.createComponentsManager(); const compName = 'my_comp'; const compId = 'c12345'; const inputHash = 'input-abc'; // Pretend that this HTML belongs to our component document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponent(compName, (data, ctx) => { throw Error('Oops!'); return 123; }); manager.registerComponentData(compName, inputHash, () => { return { hello: 'world' }; }); const result = manager.callComponent(compName, compId, inputHash); return result; }""" data = await page.evaluate(test_js) assert data is None await page.close() @with_playwright async def test_error_in_component_call_do_not_propagate_async(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] test_js: types.js = """() => { const manager = Components.createComponentsManager(); const compName = 'my_comp'; const compId = 'c12345'; const inputHash = 'input-abc'; // Pretend that this HTML belongs to our component document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponent(compName, async (data, ctx) => { throw Error('Oops!'); return 123; }); manager.registerComponentData(compName, inputHash, () => { return { hello: 'world' }; }); const result = manager.callComponent(compName, compId, inputHash); return Promise.allSettled([result]); }""" data = await page.evaluate(test_js) assert len(data) == 1 assert data[0]["status"] == "rejected" assert isinstance(data[0]["reason"], Error) assert data[0]["reason"].message == "Oops!" await page.close() @with_playwright async def test_raises_if_component_element_not_in_dom(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] test_js: types.js = """() => { const manager = Components.createComponentsManager(); const compName = 'my_comp'; const compId = '12345'; const inputHash = 'input-abc'; manager.registerComponent(compName, (data, ctx) => { return 123; }); manager.registerComponentData(compName, inputHash, () => { return { hello: 'world' }; }); // Should raise Error manager.callComponent(compName, compId, inputHash); }""" with pytest.raises( Error, match=re.escape("Error: [Components] 'my_comp': No elements with component ID '12345' found"), ): await page.evaluate(test_js) await page.close() @with_playwright async def test_raises_if_input_hash_not_registered(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] test_js: types.js = """() => { const manager = Components.createComponentsManager(); const compName = 'my_comp'; const compId = 'c12345'; const inputHash = 'input-abc'; document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponent(compName, (data, ctx) => { return Promise.resolve(123); }); // Should raise Error manager.callComponent(compName, compId, inputHash); }""" with pytest.raises( Error, match=re.escape("Error: [Components] 'my_comp': Cannot find input for hash 'input-abc'"), ): await page.evaluate(test_js) await page.close() @with_playwright async def test_raises_if_component_not_registered(self): page = await _create_page_with_dep_manager(self.browser) # type: ignore[attr-defined] test_js: types.js = """() => { const manager = Components.createComponentsManager(); const compName = 'my_comp'; const compId = '12345'; const inputHash = 'input-abc'; document.body.insertAdjacentHTML('beforeend', '
abc
'); manager.registerComponentData(compName, inputHash, () => { return { hello: 'world' }; }); // Should raise Error manager.callComponent(compName, compId, inputHash); }""" with pytest.raises( Error, match=re.escape("Error: [Components] 'my_comp': No component registered for that name"), ): await page.evaluate(test_js) await page.close()