mirror of
https://github.com/django-components/django-components.git
synced 2025-08-23 23:44:06 +00:00
fix: Fix broken JS execution order (#821)
* fix: fix broken js exec order * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * refactor: remove stale comment --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
e88e3af27f
commit
be27c1c94d
15 changed files with 833 additions and 379 deletions
|
@ -189,15 +189,15 @@ This is how we achieve that:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Load JS or CSS script if not loaded already
|
// Load JS or CSS script if not loaded already
|
||||||
Components.loadScript("js", '<script src="/abc/xyz/script.js">');
|
Components.loadJs('<script src="/abc/xyz/script.js">');
|
||||||
Components.loadScript("css", '<link href="/abc/xyz/style.css">');
|
Components.loadCss('<link href="/abc/xyz/style.css">');
|
||||||
|
|
||||||
// Or mark one as already-loaded, so it is ignored when
|
// Or mark one as already-loaded, so it is ignored when
|
||||||
// we call `loadScript`
|
// we call `loadJs`
|
||||||
Components.markScriptLoaded("js", "/abc/def");
|
Components.markScriptLoaded("js", "/abc/def");
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that `loadScript()` receives a whole `<script>` and `<link>` tags, not just the URL.
|
Note that `loadJs() / loadCss()` receive whole `<script> / <link>` tags, not just the URL.
|
||||||
This is because when Django's `Media` class renders JS and CSS, it formats it as `<script>` and `<link>` tags.
|
This is because when Django's `Media` class renders JS and CSS, it formats it as `<script>` and `<link>` tags.
|
||||||
And we allow users to modify how the JS and CSS should be rendered into the `<script>` and `<link>` tags.
|
And we allow users to modify how the JS and CSS should be rendered into the `<script>` and `<link>` tags.
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import sys
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
from textwrap import dedent
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Callable,
|
Callable,
|
||||||
|
@ -492,16 +493,25 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
|
||||||
loaded_css_urls = sorted(
|
loaded_css_urls = sorted(
|
||||||
[
|
[
|
||||||
*loaded_component_css_urls,
|
*loaded_component_css_urls,
|
||||||
# NOTE: Unlike JS, the initial CSS is loaded outside of the dependency
|
# NOTE: When rendering a document, the initial CSS is inserted directly into the HTML
|
||||||
# manager, and only marked as loaded, to avoid a flash of unstyled content.
|
# to avoid a flash of unstyled content. In the dependency manager, we only mark those
|
||||||
*to_load_css_urls,
|
# scripts as loaded.
|
||||||
|
*(to_load_css_urls if type == "document" else []),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
loaded_js_urls = sorted(
|
||||||
|
[
|
||||||
|
*loaded_component_js_urls,
|
||||||
|
# NOTE: When rendering a document, the initial JS is inserted directly into the HTML
|
||||||
|
# so the scripts are executed at proper order. In the dependency manager, we only mark those
|
||||||
|
# scripts as loaded.
|
||||||
|
*(to_load_js_urls if type == "document" else []),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
loaded_js_urls = sorted(loaded_component_js_urls)
|
|
||||||
|
|
||||||
exec_script = _gen_exec_script(
|
exec_script = _gen_exec_script(
|
||||||
to_load_js_tags=to_load_js_tags,
|
to_load_js_tags=to_load_js_tags if type == "fragment" else [],
|
||||||
to_load_css_tags=to_load_css_tags,
|
to_load_css_tags=to_load_css_tags if type == "fragment" else [],
|
||||||
loaded_js_urls=loaded_js_urls,
|
loaded_js_urls=loaded_js_urls,
|
||||||
loaded_css_urls=loaded_css_urls,
|
loaded_css_urls=loaded_css_urls,
|
||||||
)
|
)
|
||||||
|
@ -511,24 +521,37 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
|
||||||
js=[static("django_components/django_components.min.js")],
|
js=[static("django_components/django_components.min.js")],
|
||||||
).render_js()
|
).render_js()
|
||||||
|
|
||||||
final_script_tags = b"".join(
|
final_script_tags = "".join(
|
||||||
[
|
[
|
||||||
*[tag.encode("utf-8") for tag in core_script_tags],
|
# JS by us
|
||||||
*[tag.encode("utf-8") for tag in inlined_component_js_tags],
|
*[tag for tag in core_script_tags],
|
||||||
exec_script.encode("utf-8"),
|
# Make calls to the JS dependency manager
|
||||||
|
# Loads JS from `Media.js` and `Component.js` if fragment
|
||||||
|
exec_script,
|
||||||
|
# JS from `Media.js`
|
||||||
|
# NOTE: When rendering a document, the initial JS is inserted directly into the HTML
|
||||||
|
# so the scripts are executed at proper order. In the dependency manager, we only mark those
|
||||||
|
# scripts as loaded.
|
||||||
|
*(to_load_js_tags if type == "document" else []),
|
||||||
|
# JS from `Component.js` (if not fragment)
|
||||||
|
*[tag for tag in inlined_component_js_tags],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
final_css_tags = b"".join(
|
final_css_tags = "".join(
|
||||||
[
|
[
|
||||||
*[tag.encode("utf-8") for tag in inlined_component_css_tags],
|
# CSS by us
|
||||||
# NOTE: Unlike JS, the initial CSS is loaded outside of the dependency
|
# <NONE>
|
||||||
# manager, and only marked as loaded, to avoid a flash of unstyled content.
|
# CSS from `Component.css` (if not fragment)
|
||||||
*[tag.encode("utf-8") for tag in to_load_css_tags],
|
*[tag for tag in inlined_component_css_tags],
|
||||||
|
# CSS from `Media.css` (plus from `Component.css` if fragment)
|
||||||
|
# NOTE: Similarly to JS, the initial CSS is loaded outside of the dependency
|
||||||
|
# manager, and only marked as loaded, to avoid a flash of unstyled content.
|
||||||
|
*[tag for tag in to_load_css_tags],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return content, final_script_tags, final_css_tags
|
return (content, final_script_tags.encode("utf-8"), final_css_tags.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def _is_nonempty_str(txt: Optional[str]) -> bool:
|
def _is_nonempty_str(txt: Optional[str]) -> bool:
|
||||||
|
@ -551,8 +574,9 @@ def _postprocess_media_tags(
|
||||||
|
|
||||||
if not _is_nonempty_str(maybe_url):
|
if not _is_nonempty_str(maybe_url):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"One of entries for `Component.Media.{script_type}` media is missing "
|
f"One of entries for `Component.Media.{script_type}` media is missing a "
|
||||||
f"value for attribute '{attr}'. Got:\n{tag}"
|
f"value for attribute '{attr}'. If there is content inlined inside the `<{node.tag}>` tags, "
|
||||||
|
f"you must move the content to a `.{script_type}` file and reference it via '{attr}'.\nGot:\n{tag}"
|
||||||
)
|
)
|
||||||
|
|
||||||
url = cast(str, maybe_url)
|
url = cast(str, maybe_url)
|
||||||
|
@ -595,21 +619,21 @@ def _prepare_tags_and_urls(
|
||||||
if type == "document":
|
if type == "document":
|
||||||
# NOTE: Skip fetching of inlined JS/CSS if it's not defined or empty for given component
|
# NOTE: Skip fetching of inlined JS/CSS if it's not defined or empty for given component
|
||||||
if _is_nonempty_str(comp_cls.js):
|
if _is_nonempty_str(comp_cls.js):
|
||||||
inlined_js_tags.append(_get_script("js", comp_cls, type="tag"))
|
inlined_js_tags.append(_get_script_tag("js", comp_cls))
|
||||||
loaded_js_urls.append(_get_script("js", comp_cls, type="url"))
|
loaded_js_urls.append(get_script_url("js", comp_cls))
|
||||||
|
|
||||||
if _is_nonempty_str(comp_cls.css):
|
if _is_nonempty_str(comp_cls.css):
|
||||||
inlined_css_tags.append(_get_script("css", comp_cls, type="tag"))
|
inlined_css_tags.append(_get_script_tag("css", comp_cls))
|
||||||
loaded_css_urls.append(_get_script("css", comp_cls, type="url"))
|
loaded_css_urls.append(get_script_url("css", comp_cls))
|
||||||
|
|
||||||
# When NOT a document (AKA is a fragment), then scripts are NOT inserted into
|
# When NOT a document (AKA is a fragment), then scripts are NOT inserted into
|
||||||
# the HTML, and instead we fetch and load them all via our JS dependency manager.
|
# the HTML, and instead we fetch and load them all via our JS dependency manager.
|
||||||
else:
|
else:
|
||||||
if _is_nonempty_str(comp_cls.js):
|
if _is_nonempty_str(comp_cls.js):
|
||||||
to_load_js_urls.append(_get_script("js", comp_cls, type="url"))
|
to_load_js_urls.append(get_script_url("js", comp_cls))
|
||||||
|
|
||||||
if _is_nonempty_str(comp_cls.css):
|
if _is_nonempty_str(comp_cls.css):
|
||||||
loaded_css_urls.append(_get_script("css", comp_cls, type="url"))
|
loaded_css_urls.append(get_script_url("css", comp_cls))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
to_load_js_urls,
|
to_load_js_urls,
|
||||||
|
@ -621,32 +645,45 @@ def _prepare_tags_and_urls(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_script(
|
def get_script_content(
|
||||||
script_type: ScriptType,
|
script_type: ScriptType,
|
||||||
comp_cls: Type["Component"],
|
comp_cls: Type["Component"],
|
||||||
type: Literal["url", "tag"],
|
) -> SafeString:
|
||||||
) -> Union[str, SafeString]:
|
comp_cls_hash = _hash_comp_cls(comp_cls)
|
||||||
|
cache_key = _gen_cache_key(comp_cls_hash, script_type)
|
||||||
|
script = comp_media_cache.get(cache_key)
|
||||||
|
|
||||||
|
return script
|
||||||
|
|
||||||
|
|
||||||
|
def _get_script_tag(
|
||||||
|
script_type: ScriptType,
|
||||||
|
comp_cls: Type["Component"],
|
||||||
|
) -> SafeString:
|
||||||
|
script = get_script_content(script_type, comp_cls)
|
||||||
|
|
||||||
|
if script_type == "js":
|
||||||
|
return f"<script>{_escape_js(script)}</script>"
|
||||||
|
|
||||||
|
elif script_type == "css":
|
||||||
|
return f"<style>{script}</style>"
|
||||||
|
|
||||||
|
return script
|
||||||
|
|
||||||
|
|
||||||
|
def get_script_url(
|
||||||
|
script_type: ScriptType,
|
||||||
|
comp_cls: Type["Component"],
|
||||||
|
) -> str:
|
||||||
comp_cls_hash = _hash_comp_cls(comp_cls)
|
comp_cls_hash = _hash_comp_cls(comp_cls)
|
||||||
|
|
||||||
if type == "url":
|
return reverse(
|
||||||
# NOTE: To make sure that Media object won't modify the URLs, we need to
|
CACHE_ENDPOINT_NAME,
|
||||||
# resolve the full path (`/abc/def/etc`), not just the file name.
|
kwargs={
|
||||||
script = reverse(
|
"comp_cls_hash": comp_cls_hash,
|
||||||
CACHE_ENDPOINT_NAME,
|
"script_type": script_type,
|
||||||
kwargs={
|
},
|
||||||
"comp_cls_hash": comp_cls_hash,
|
)
|
||||||
"script_type": script_type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cache_key = _gen_cache_key(comp_cls_hash, script_type)
|
|
||||||
script = comp_media_cache.get(cache_key)
|
|
||||||
|
|
||||||
if script_type == "js":
|
|
||||||
script = mark_safe(f"<script>{_escape_js(script)}</script>")
|
|
||||||
elif script_type == "css":
|
|
||||||
script = mark_safe(f"<style>{script}</style>")
|
|
||||||
return script
|
|
||||||
|
|
||||||
|
|
||||||
def _gen_exec_script(
|
def _gen_exec_script(
|
||||||
|
@ -658,9 +695,9 @@ def _gen_exec_script(
|
||||||
# Generate JS expression like so:
|
# Generate JS expression like so:
|
||||||
# ```js
|
# ```js
|
||||||
# Promise.all([
|
# Promise.all([
|
||||||
# Components.manager.loadScript("js", '<script src="/abc/def1">...</script>'),
|
# Components.manager.loadJs('<script src="/abc/def1">...</script>'),
|
||||||
# Components.manager.loadScript("js", '<script src="/abc/def2">...</script>'),
|
# Components.manager.loadJs('<script src="/abc/def2">...</script>'),
|
||||||
# Components.manager.loadScript("css", '<link href="/abc/def3">'),
|
# Components.manager.loadCss('<link href="/abc/def3">'),
|
||||||
# ]);
|
# ]);
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
|
@ -672,13 +709,9 @@ def _gen_exec_script(
|
||||||
# Components.manager.markScriptLoaded("js", "/abc/def3.js"),
|
# Components.manager.markScriptLoaded("js", "/abc/def3.js"),
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
# NOTE: It would be better to pass only the URL itself for `loadScript`, instead of a whole tag.
|
# NOTE: It would be better to pass only the URL itself for `loadJs/loadCss`, instead of a whole tag.
|
||||||
# But because we allow users to specify the Media class, and thus users can
|
# But because we allow users to specify the Media class, and thus users can
|
||||||
# configure how the `<link>` or `<script>` tags are rendered, we need pass the whole tag.
|
# configure how the `<link>` or `<script>` tags are rendered, we need pass the whole tag.
|
||||||
#
|
|
||||||
# NOTE 2: We must NOT await for the Promises, otherwise we create a deadlock
|
|
||||||
# where the script loaded with `loadScript` (loadee) is inserted AFTER the script with `loadScript` (loader).
|
|
||||||
# But the loader will NOT finish, because it's waiting for loadee, which cannot start before loader ends.
|
|
||||||
escaped_to_load_js_tags = [_escape_js(tag, eval=False) for tag in to_load_js_tags]
|
escaped_to_load_js_tags = [_escape_js(tag, eval=False) for tag in to_load_js_tags]
|
||||||
escaped_to_load_css_tags = [_escape_js(tag, eval=False) for tag in to_load_css_tags]
|
escaped_to_load_css_tags = [_escape_js(tag, eval=False) for tag in to_load_css_tags]
|
||||||
|
|
||||||
|
@ -686,25 +719,23 @@ def _gen_exec_script(
|
||||||
def js_arr(lst: List) -> str:
|
def js_arr(lst: List) -> str:
|
||||||
return "[" + ", ".join(lst) + "]"
|
return "[" + ", ".join(lst) + "]"
|
||||||
|
|
||||||
exec_script: types.js = f"""(() => {{
|
# NOTE: Wrap the body in self-executing function to avoid polluting the global scope.
|
||||||
const loadedJsScripts = {json.dumps(loaded_js_urls)};
|
exec_script: types.js = f"""
|
||||||
const loadedCssScripts = {json.dumps(loaded_css_urls)};
|
(() => {{
|
||||||
const toLoadJsScripts = {js_arr(escaped_to_load_js_tags)};
|
Components.manager._loadComponentScripts({{
|
||||||
const toLoadCssScripts = {js_arr(escaped_to_load_css_tags)};
|
loadedCssUrls: {json.dumps(loaded_css_urls)},
|
||||||
|
loadedJsUrls: {json.dumps(loaded_js_urls)},
|
||||||
loadedJsScripts.forEach((s) => Components.manager.markScriptLoaded("js", s));
|
toLoadCssTags: {js_arr(escaped_to_load_css_tags)},
|
||||||
loadedCssScripts.forEach((s) => Components.manager.markScriptLoaded("css", s));
|
toLoadJsTags: {js_arr(escaped_to_load_js_tags)},
|
||||||
|
}});
|
||||||
Promise.all(
|
document.currentScript.remove();
|
||||||
toLoadJsScripts.map((s) => Components.manager.loadScript("js", s))
|
}})();
|
||||||
).catch(console.error);
|
|
||||||
Promise.all(
|
|
||||||
toLoadCssScripts.map((s) => Components.manager.loadScript("css", s))
|
|
||||||
).catch(console.error);
|
|
||||||
}})();
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
exec_script = f"<script>{_escape_js(exec_script)}</script>"
|
# NOTE: The exec script MUST be executed SYNC, so we MUST NOT put `type="module"`,
|
||||||
|
# `async`, nor `defer` on it.
|
||||||
|
# See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||||
|
exec_script = f"<script>{_escape_js(dedent(exec_script))}</script>"
|
||||||
return exec_script
|
return exec_script
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
(()=>{var y=Array.isArray,p=t=>typeof t=="function",M=t=>t!==null&&typeof t=="object",h=t=>(M(t)||p(t))&&p(t.then)&&p(t.catch);function S(t,a){try{return a?t.apply(null,a):t()}catch(r){f(r)}}function m(t,a){if(p(t)){let r=S(t,a);return r&&h(r)&&r.catch(i=>{f(i)}),[r]}if(y(t)){let r=[];for(let i=0;i<t.length;i++)r.push(m(t[i],a));return r}else console.warn(`[Components] Invalid value type passed to callWithAsyncErrorHandling(): ${typeof t}`)}function f(t){console.error(t)}var u=()=>{let t=new Set,a=new Set,r={},i={},C=e=>{let n=new DOMParser().parseFromString(e,"text/html").querySelector("script");if(!n)throw Error("[Components] Failed to extract <script> tag. Make sure that the string contains <script><\/script> and is a valid HTML");return n},x=e=>{let n=new DOMParser().parseFromString(e,"text/html").querySelector("link");if(!n)throw Error("[Components] Failed to extract <link> tag. Make sure that the string contains <link></link> and is a valid HTML");return n},g=e=>{let n=document.createElement(e.tagName);for(let o of e.attributes)n.setAttributeNode(o.cloneNode());return n};return{callComponent:(e,n,o)=>{let s=r[e];if(!s)throw Error(`[Components] '${e}': No component registered for that name`);let c=Array.from(document.querySelectorAll(`[data-comp-id-${n}]`));if(!c.length)throw Error(`[Components] '${e}': No elements with component ID '${n}' found`);let l=`${e}:${o}`,d=i[l];if(!d)throw Error(`[Components] '${e}': Cannot find input for hash '${o}'`);let T=d(),E={name:e,id:n,els:c},[F]=m(s,[T,E]);return F},registerComponent:(e,n)=>{r[e]=n},registerComponentData:(e,n,o)=>{let s=`${e}:${n}`;i[s]=o},loadScript:(e,n)=>{if(e==="js"){let o=C(n),s=o.getAttribute("src");if(!s||t.has(s))return;t.add(s);let c=g(o);return new Promise((l,d)=>{c.onload=()=>{l()},globalThis.document.body.append(c)})}else if(e==="css"){let o=x(n),s=o.getAttribute("href");if(!s||a.has(s))return;let c=g(o);return globalThis.document.head.append(c),a.add(s),Promise.resolve()}else throw Error(`[Components] loadScript received invalid script type '${e}'. Must be one of 'js', 'css'`)},markScriptLoaded:(e,n)=>{if(e==="js")t.add(n);else if(e==="css")a.add(n);else throw Error(`[Components] markScriptLoaded received invalid script type '${e}'. Must be one of 'js', 'css'`)}}};var w={manager:u(),createComponentsManager:u,unescapeJs:r=>new DOMParser().parseFromString(r,"text/html").documentElement.textContent};globalThis.Components=w;})();
|
(()=>{var x=Array.isArray,l=n=>typeof n=="function",w=n=>n!==null&&typeof n=="object",E=n=>(w(n)||l(n))&&l(n.then)&&l(n.catch);function j(n,a){try{return a?n.apply(null,a):n()}catch(r){S(r)}}function g(n,a){if(l(n)){let r=j(n,a);return r&&E(r)&&r.catch(i=>{S(i)}),[r]}if(x(n)){let r=[];for(let i=0;i<n.length;i++)r.push(g(n[i],a));return r}else console.warn(`[Components] Invalid value type passed to callWithAsyncErrorHandling(): ${typeof n}`)}function S(n){console.error(n)}var u=()=>{let n=new Set,a=new Set,r={},i={},b=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("script");if(!e)throw Error("[Components] Failed to extract <script> tag. Make sure that the string contains <script><\/script> and is a valid HTML");return e},M=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("link");if(!e)throw Error("[Components] Failed to extract <link> tag. Make sure that the string contains <link></link> and is a valid HTML");return e},y=t=>{let e=document.createElement(t.tagName);e.innerHTML=t.innerHTML;for(let o of t.attributes)e.setAttributeNode(o.cloneNode());return e},h=t=>{let e=b(t),o=e.getAttribute("src");if(!o||T("js",o))return;c("js",o);let s=y(e),p=e.getAttribute("async")!=null||e.getAttribute("defer")!=null||e.getAttribute("type")==="module";s.async=p;let m=new Promise((d,f)=>{s.onload=()=>{d()},globalThis.document.body.append(s)});return{el:s,promise:m}},C=t=>{let e=M(t),o=e.getAttribute("href");if(!o||T("css",o))return;let s=y(e);return globalThis.document.head.append(s),c("css",o),{el:s,promise:Promise.resolve()}},c=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] markScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);(t==="js"?n:a).add(e)},T=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] isScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);return(t==="js"?n:a).has(e)};return{callComponent:(t,e,o)=>{let s=r[t];if(!s)throw Error(`[Components] '${t}': No component registered for that name`);let p=Array.from(document.querySelectorAll(`[data-comp-id-${e}]`));if(!p.length)throw Error(`[Components] '${t}': No elements with component ID '${e}' found`);let m=`${t}:${o}`,d=i[m];if(!d)throw Error(`[Components] '${t}': Cannot find input for hash '${o}'`);let f=d(),F={name:t,id:e,els:p},[L]=g(s,[f,F]);return L},registerComponent:(t,e)=>{r[t]=e},registerComponentData:(t,e,o)=>{let s=`${t}:${e}`;i[s]=o},loadJs:h,loadCss:C,markScriptLoaded:c,_loadComponentScripts:async t=>{t.loadedCssUrls.forEach(o=>c("css",o)),t.loadedJsUrls.forEach(o=>c("js",o)),Promise.all(t.toLoadCssTags.map(o=>C(o))).catch(console.error);let e=Promise.all(t.toLoadJsTags.map(o=>h(o))).catch(console.error)}}};var k={manager:u(),createComponentsManager:u,unescapeJs:r=>new DOMParser().parseFromString(r,"text/html").documentElement.textContent};globalThis.Components=k;})();
|
||||||
|
|
|
@ -32,10 +32,10 @@ Components.callComponent(
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load JS or CSS script if not laoded already
|
// Load JS or CSS script if not laoded already
|
||||||
Components.loadScript("js", '<script src="/abc/def">');
|
Components.loadJs('<script src="/abc/def">');
|
||||||
|
|
||||||
// Or mark one as already-loaded, so it is ignored when
|
// Or mark one as already-loaded, so it is ignored when
|
||||||
// we call `loadScript`
|
// we call `loadJs`
|
||||||
Components.markScriptLoaded("js", '/abc/def');
|
Components.markScriptLoaded("js", '/abc/def');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,11 @@ export type ScriptType = 'js' | 'css';
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* Components.loadScript("js", '<script src="/abc/def"></script>');
|
* Components.loadJs('<script src="/abc/def"></script>');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* Components.loadCss('<link href="/abc/def" />');
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
|
@ -84,68 +88,104 @@ export const createComponentsManager = () => {
|
||||||
// one to the other.
|
// one to the other.
|
||||||
// Might be related to https://security.stackexchange.com/a/240362/302733
|
// Might be related to https://security.stackexchange.com/a/240362/302733
|
||||||
// See https://stackoverflow.com/questions/13121948
|
// See https://stackoverflow.com/questions/13121948
|
||||||
const cloneNode = (srcNode: HTMLElement) => {
|
const cloneNode = <T extends HTMLElement>(srcNode: T): T => {
|
||||||
const targetNode = document.createElement(srcNode.tagName);
|
const targetNode = document.createElement(srcNode.tagName) as T;
|
||||||
|
targetNode.innerHTML = srcNode.innerHTML;
|
||||||
for (const attr of srcNode.attributes) {
|
for (const attr of srcNode.attributes) {
|
||||||
targetNode.setAttributeNode(attr.cloneNode() as Attr);
|
targetNode.setAttributeNode(attr.cloneNode() as Attr);
|
||||||
}
|
}
|
||||||
return targetNode;
|
return targetNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadScript = (type: ScriptType, tag: string) => {
|
const loadJs = (tag: string) => {
|
||||||
if (type === 'js') {
|
const srcScriptNode = parseScriptTag(tag);
|
||||||
const srcScriptNode = parseScriptTag(tag);
|
|
||||||
|
|
||||||
// Use `.getAttribute()` instead of `.src` so we get the value as is,
|
// Use `.getAttribute()` instead of `.src` so we get the value as is,
|
||||||
// without the host name prepended if URL is just a path.
|
// without the host name prepended if URL is just a path.
|
||||||
const src = srcScriptNode.getAttribute('src');
|
const src = srcScriptNode.getAttribute('src');
|
||||||
if (!src || loadedJs.has(src)) return;
|
if (!src || isScriptLoaded('js', src)) return;
|
||||||
|
|
||||||
loadedJs.add(src);
|
markScriptLoaded('js', src);
|
||||||
|
|
||||||
const targetScriptNode = cloneNode(srcScriptNode);
|
const targetScriptNode = cloneNode(srcScriptNode);
|
||||||
|
|
||||||
// In case of JS scripts, we return a Promise that resolves when the script is loaded
|
const isAsync = (
|
||||||
// See https://stackoverflow.com/a/57267538/9788634
|
// NOTE: `async` and `defer` are boolean attributes, so their value can be
|
||||||
return new Promise<void>((resolve, reject) => {
|
// an empty string, hence the `!= null` check.
|
||||||
targetScriptNode.onload = () => {
|
// Read more on https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
|
||||||
resolve();
|
srcScriptNode.getAttribute('async') != null
|
||||||
};
|
|| srcScriptNode.getAttribute('defer') != null
|
||||||
|
|| srcScriptNode.getAttribute('type') === 'module'
|
||||||
|
);
|
||||||
|
|
||||||
// Insert the script at the end of <body> to follow convention
|
// Setting this to `false` ensures that the loading and execution of the script is "blocking",
|
||||||
globalThis.document.body.append(targetScriptNode);
|
// meaning that the next script in line will wait until this one is done.
|
||||||
});
|
// See https://stackoverflow.com/a/21550322/9788634
|
||||||
} else if (type === 'css') {
|
targetScriptNode.async = isAsync;
|
||||||
const linkNode = parseLinkTag(tag);
|
|
||||||
// NOTE: Use `.getAttribute()` instead of `.href` so we get the value as is,
|
|
||||||
// without the host name prepended if URL is just a path.
|
|
||||||
const href = linkNode.getAttribute('href');
|
|
||||||
if (!href || loadedCss.has(href)) return;
|
|
||||||
|
|
||||||
// Insert at the end of <head> to follow convention
|
// In case of JS scripts, we return a Promise that resolves when the script is loaded
|
||||||
const targetLinkNode = cloneNode(linkNode);
|
// See https://stackoverflow.com/a/57267538/9788634
|
||||||
globalThis.document.head.append(targetLinkNode);
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
loadedCss.add(href);
|
targetScriptNode.onload = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
// For CSS, we return a dummy Promise, since we don't need to wait for anything
|
// Insert at the end of `<body>` to follow convention
|
||||||
return Promise.resolve();
|
//
|
||||||
} else {
|
// NOTE: Because we are inserting the script into the DOM from within JS,
|
||||||
throw Error(
|
// the order of execution of the inserted scripts behaves a bit different:
|
||||||
`[Components] loadScript received invalid script type '${type}'. Must be one of 'js', 'css'`
|
// - The `<script>` that were originally in the HTML file will run in the order they appear in the file.
|
||||||
);
|
// And they will run BEFORE the dynamically inserted scripts.
|
||||||
}
|
// - The order of execution of the dynamically inserted scripts depends on the order of INSERTION,
|
||||||
|
// and NOT on WHERE we insert the script in the DOM.
|
||||||
|
globalThis.document.body.append(targetScriptNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
el: targetScriptNode,
|
||||||
|
promise,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const markScriptLoaded = (type: ScriptType, url: string) => {
|
const loadCss = (tag: string) => {
|
||||||
if (type === 'js') {
|
const linkNode = parseLinkTag(tag);
|
||||||
loadedJs.add(url);
|
// NOTE: Use `.getAttribute()` instead of `.href` so we get the value as is,
|
||||||
} else if (type === 'css') {
|
// without the host name prepended if URL is just a path.
|
||||||
loadedCss.add(url);
|
const href = linkNode.getAttribute('href');
|
||||||
} else {
|
if (!href || isScriptLoaded('css', href)) return;
|
||||||
|
|
||||||
|
// Insert at the end of <head> to follow convention
|
||||||
|
const targetLinkNode = cloneNode(linkNode);
|
||||||
|
globalThis.document.head.append(targetLinkNode);
|
||||||
|
markScriptLoaded('css', href);
|
||||||
|
|
||||||
|
// For CSS, we return a dummy Promise, since we don't need to wait for anything
|
||||||
|
return {
|
||||||
|
el: targetLinkNode,
|
||||||
|
promise: Promise.resolve(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const markScriptLoaded = (type: ScriptType, url: string): void => {
|
||||||
|
if (type !== 'js' && type !== 'css') {
|
||||||
throw Error(
|
throw Error(
|
||||||
`[Components] markScriptLoaded received invalid script type '${type}'. Must be one of 'js', 'css'`
|
`[Components] markScriptLoaded received invalid script type '${type}'. Must be one of 'js', 'css'`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const urlsSet = type === 'js' ? loadedJs : loadedCss;
|
||||||
|
urlsSet.add(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isScriptLoaded = (type: ScriptType, url: string): boolean => {
|
||||||
|
if (type !== 'js' && type !== 'css') {
|
||||||
|
throw Error(
|
||||||
|
`[Components] isScriptLoaded received invalid script type '${type}'. Must be one of 'js', 'css'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlsSet = type === 'js' ? loadedJs : loadedCss;
|
||||||
|
return urlsSet.has(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerComponent = (name: string, compFn: ComponentFn) => {
|
const registerComponent = (name: string, compFn: ComponentFn) => {
|
||||||
|
@ -186,11 +226,38 @@ export const createComponentsManager = () => {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Internal API - We call this when we want to load / register all JS & CSS files rendered by component(s) */
|
||||||
|
const _loadComponentScripts = async (inputs: {
|
||||||
|
loadedCssUrls: string[];
|
||||||
|
loadedJsUrls: string[];
|
||||||
|
toLoadCssTags: string[];
|
||||||
|
toLoadJsTags: string[];
|
||||||
|
}) => {
|
||||||
|
// Mark as loaded the CSS that WAS inlined into the HTML.
|
||||||
|
inputs.loadedCssUrls.forEach((s) => markScriptLoaded("css", s));
|
||||||
|
inputs.loadedJsUrls.forEach((s) => markScriptLoaded("js", s));
|
||||||
|
|
||||||
|
// Load CSS that was not inlined into the HTML
|
||||||
|
// NOTE: We don't need to wait for CSS to load
|
||||||
|
Promise
|
||||||
|
.all(inputs.toLoadCssTags.map((s) => loadCss(s)))
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
// Load JS that was not inlined into the HTML
|
||||||
|
const jsScriptsPromise = Promise
|
||||||
|
// NOTE: Interestingly enough, when we insert scripts into the DOM programmatically,
|
||||||
|
// the order of execution is the same as the order of insertion.
|
||||||
|
.all(inputs.toLoadJsTags.map((s) => loadJs(s)))
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
callComponent,
|
callComponent,
|
||||||
registerComponent,
|
registerComponent,
|
||||||
registerComponentData,
|
registerComponentData,
|
||||||
loadScript,
|
loadJs,
|
||||||
|
loadCss,
|
||||||
markScriptLoaded,
|
markScriptLoaded,
|
||||||
|
_loadComponentScripts,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -78,3 +78,65 @@ class OtherComponent(Component):
|
||||||
class Media:
|
class Media:
|
||||||
css = "style.css"
|
css = "style.css"
|
||||||
js = "script.js"
|
js = "script.js"
|
||||||
|
|
||||||
|
|
||||||
|
@register("check_script_order_in_js")
|
||||||
|
class CheckScriptOrderInJs(Component):
|
||||||
|
template = "<check_script_order>"
|
||||||
|
|
||||||
|
# We should be able to access the global variables set by the previous components:
|
||||||
|
# Scripts:
|
||||||
|
# - script.js - testMsg
|
||||||
|
# - script2.js - testMsg2
|
||||||
|
# Components:
|
||||||
|
# - SimpleComponent - testSimpleComponent
|
||||||
|
# - SimpleComponentNested - testSimpleComponentNested
|
||||||
|
# - OtherComponent - testOtherComponent
|
||||||
|
js: types.js = """
|
||||||
|
globalThis.checkVars = {
|
||||||
|
testSimpleComponent,
|
||||||
|
testSimpleComponentNested,
|
||||||
|
testOtherComponent,
|
||||||
|
testMsg,
|
||||||
|
testMsg2,
|
||||||
|
};
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@register("check_script_order_in_media")
|
||||||
|
class CheckScriptOrderInMedia(Component):
|
||||||
|
template = "<check_script_order>"
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = "check_script_order.js"
|
||||||
|
|
||||||
|
|
||||||
|
@register("alpine_test_in_media")
|
||||||
|
class AlpineCompInMedia(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div x-data="alpine_test">
|
||||||
|
ALPINE_TEST:
|
||||||
|
<div x-text="somevalue"></div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = "alpine_test.js"
|
||||||
|
|
||||||
|
|
||||||
|
@register("alpine_test_in_js")
|
||||||
|
class AlpineCompInJs(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div x-data="alpine_test">
|
||||||
|
ALPINE_TEST:
|
||||||
|
<div x-text="somevalue"></div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
js: types.js = """
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('alpine_test', () => ({
|
||||||
|
somevalue: 123,
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
"""
|
||||||
|
|
5
tests/e2e/testserver/testserver/static/alpine_test.js
Normal file
5
tests/e2e/testserver/testserver/static/alpine_test.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('alpine_test', () => ({
|
||||||
|
somevalue: 123,
|
||||||
|
}))
|
||||||
|
});
|
16
tests/e2e/testserver/testserver/static/check_script_order.js
Normal file
16
tests/e2e/testserver/testserver/static/check_script_order.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// We should be able to access the global variables set by the previous components:
|
||||||
|
// Scripts:
|
||||||
|
// - script.js - testMsg
|
||||||
|
// - script2.js - testMsg2
|
||||||
|
// Components:
|
||||||
|
// - SimpleComponent - testSimpleComponent
|
||||||
|
// - SimpleComponentNested - testSimpleComponentNested
|
||||||
|
// - OtherComponent - testOtherComponent
|
||||||
|
|
||||||
|
globalThis.checkVars = {
|
||||||
|
testSimpleComponent: globalThis.testSimpleComponent,
|
||||||
|
testSimpleComponentNested: globalThis.testSimpleComponentNested,
|
||||||
|
testOtherComponent: globalThis.testOtherComponent,
|
||||||
|
testMsg: globalThis.testMsg,
|
||||||
|
testMsg2: globalThis.testMsg2,
|
||||||
|
};
|
|
@ -1,11 +1,29 @@
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from testserver.views import multiple_components_view, single_component_view
|
from testserver.views import (
|
||||||
|
alpine_in_body_vars_not_available_before_view,
|
||||||
|
alpine_in_body_view,
|
||||||
|
alpine_in_body_view_2,
|
||||||
|
alpine_in_head_view,
|
||||||
|
check_js_order_in_js_view,
|
||||||
|
check_js_order_in_media_view,
|
||||||
|
check_js_order_vars_not_available_before_view,
|
||||||
|
multiple_components_view,
|
||||||
|
single_component_view,
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("single/", single_component_view, name="single"),
|
|
||||||
path("multi/", multiple_components_view, name="multi"),
|
|
||||||
path("", include("django_components.urls")),
|
path("", include("django_components.urls")),
|
||||||
# Empty response with status 200 to notify other systems when the server has started
|
# Empty response with status 200 to notify other systems when the server has started
|
||||||
path("poll/", lambda *args, **kwargs: HttpResponse("")),
|
path("poll/", lambda *args, **kwargs: HttpResponse("")),
|
||||||
|
# Test views
|
||||||
|
path("single/", single_component_view, name="single"),
|
||||||
|
path("multi/", multiple_components_view, name="multi"),
|
||||||
|
path("js-order/js", check_js_order_in_js_view),
|
||||||
|
path("js-order/media", check_js_order_in_media_view),
|
||||||
|
path("js-order/invalid", check_js_order_vars_not_available_before_view),
|
||||||
|
path("alpine/head", alpine_in_head_view),
|
||||||
|
path("alpine/body", alpine_in_body_view),
|
||||||
|
path("alpine/body2", alpine_in_body_view_2),
|
||||||
|
path("alpine/invalid", alpine_in_body_vars_not_available_before_view),
|
||||||
]
|
]
|
||||||
|
|
|
@ -49,3 +49,164 @@ def multiple_components_view(request):
|
||||||
rendered_raw = template.render(Context({}))
|
rendered_raw = template.render(Context({}))
|
||||||
rendered = render_dependencies(rendered_raw)
|
rendered = render_dependencies(rendered_raw)
|
||||||
return HttpResponse(rendered)
|
return HttpResponse(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
def check_js_order_in_js_view(request):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% component 'outer' variable='variable' %}
|
||||||
|
{% component 'other' variable='variable_inner' / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{# check_script_order_in_media is AFTER the other components #}
|
||||||
|
{% component 'check_script_order_in_js' / %}
|
||||||
|
abc
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered_raw = template.render(Context({}))
|
||||||
|
rendered = render_dependencies(rendered_raw)
|
||||||
|
return HttpResponse(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
def check_js_order_in_media_view(request):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% component 'outer' variable='variable' %}
|
||||||
|
{% component 'other' variable='variable_inner' / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{# check_script_order_in_media is AFTER the other components #}
|
||||||
|
{% component 'check_script_order_in_media' / %}
|
||||||
|
abc
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered_raw = template.render(Context({}))
|
||||||
|
rendered = render_dependencies(rendered_raw)
|
||||||
|
return HttpResponse(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
def check_js_order_vars_not_available_before_view(request):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{# check_script_order_in_media is BEFORE the other components #}
|
||||||
|
{% component 'check_script_order_in_media' / %}
|
||||||
|
{% component 'outer' variable='variable' %}
|
||||||
|
{% component 'other' variable='variable_inner' / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
abc
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered_raw = template.render(Context({}))
|
||||||
|
rendered = render_dependencies(rendered_raw)
|
||||||
|
return HttpResponse(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
def alpine_in_head_view(request):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
<script defer src="https://unpkg.com/alpinejs"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% component 'alpine_test_in_media' / %}
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered_raw = template.render(Context({}))
|
||||||
|
rendered = render_dependencies(rendered_raw)
|
||||||
|
return HttpResponse(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
def alpine_in_body_view(request):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% component 'alpine_test_in_media' / %}
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
<script src="https://unpkg.com/alpinejs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered_raw = template.render(Context({}))
|
||||||
|
rendered = render_dependencies(rendered_raw)
|
||||||
|
return HttpResponse(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
# Same as before, but Alpine component defined in Component.js
|
||||||
|
def alpine_in_body_view_2(request):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% component 'alpine_test_in_js' / %}
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
<script src="https://unpkg.com/alpinejs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered_raw = template.render(Context({}))
|
||||||
|
rendered = render_dependencies(rendered_raw)
|
||||||
|
return HttpResponse(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
def alpine_in_body_vars_not_available_before_view(request):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% component 'alpine_test_in_js' / %}
|
||||||
|
{# Alpine loaded BEFORE components JS #}
|
||||||
|
<script src="https://unpkg.com/alpinejs"></script>
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered_raw = template.render(Context({}))
|
||||||
|
rendered = render_dependencies(rendered_raw)
|
||||||
|
return HttpResponse(rendered)
|
||||||
|
|
|
@ -125,11 +125,7 @@ class ComponentMediaTests(BaseTestCase):
|
||||||
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
|
||||||
self.assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style2.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script src="path/to/script.js"></script>', rendered)
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_css_js_as_string(self):
|
def test_css_js_as_string(self):
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
|
@ -146,12 +142,7 @@ class ComponentMediaTests(BaseTestCase):
|
||||||
rendered = SimpleComponent.render()
|
rendered = SimpleComponent.render()
|
||||||
|
|
||||||
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
self.assertInHTML('<script src="path/to/script.js"></script>', rendered)
|
||||||
# Command to load the JS from Media.js
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_css_as_dict(self):
|
def test_css_as_dict(self):
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
|
@ -175,11 +166,7 @@ class ComponentMediaTests(BaseTestCase):
|
||||||
self.assertInHTML('<link href="path/to/style2.css" media="print" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style2.css" media="print" rel="stylesheet">', rendered)
|
||||||
self.assertInHTML('<link href="path/to/style3.css" media="screen" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style3.css" media="screen" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script src="path/to/script.js"></script>', rendered)
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_media_custom_render_js(self):
|
def test_media_custom_render_js(self):
|
||||||
class MyMedia(Media):
|
class MyMedia(Media):
|
||||||
|
@ -204,15 +191,8 @@ class ComponentMediaTests(BaseTestCase):
|
||||||
|
|
||||||
rendered = SimpleComponent.render()
|
rendered = SimpleComponent.render()
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertIn('<script defer src="path/to/script.js"></script>', rendered)
|
||||||
self.assertIn(
|
self.assertIn('<script defer src="path/to/script2.js"></script>', rendered)
|
||||||
"Components.unescapeJs(\\`&lt;script defer src=&quot;path/to/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script defer src=&quot;path/to/script2.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_media_custom_render_css(self):
|
def test_media_custom_render_css(self):
|
||||||
class MyMedia(Media):
|
class MyMedia(Media):
|
||||||
|
@ -314,23 +294,10 @@ class MediaPathAsObjectTests(BaseTestCase):
|
||||||
self.assertInHTML('<link css_tag href="path/to/style3.css" rel="stylesheet" />', rendered)
|
self.assertInHTML('<link css_tag href="path/to/style3.css" rel="stylesheet" />', rendered)
|
||||||
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script js_tag src="path/to/script.js" type="module"></script>', rendered)
|
||||||
self.assertIn(
|
self.assertInHTML('<script hi src="path/to/script2.js"></script>', rendered)
|
||||||
"Components.unescapeJs(\\`&lt;script js_tag src=&quot;path/to/script.js&quot; type=&quot;module&quot;&gt;&lt;/script&gt;\\`)",
|
self.assertInHTML('<script type="module" src="path/to/script3.js"></script>', rendered)
|
||||||
rendered,
|
self.assertInHTML('<script src="path/to/script4.js"></script>', rendered)
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script hi src=&quot;path/to/script2.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script type=&quot;module&quot; src=&quot;path/to/script3.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script4.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_pathlike(self):
|
def test_pathlike(self):
|
||||||
"""
|
"""
|
||||||
|
@ -376,19 +343,9 @@ class MediaPathAsObjectTests(BaseTestCase):
|
||||||
self.assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered)
|
||||||
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script src="path/to/script.js"></script>', rendered)
|
||||||
self.assertIn(
|
self.assertInHTML('<script src="path/to/script2.js"></script>', rendered)
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
self.assertInHTML('<script src="path/to/script3.js"></script>', rendered)
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script2.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script3.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
"""
|
"""
|
||||||
|
@ -429,15 +386,8 @@ class MediaPathAsObjectTests(BaseTestCase):
|
||||||
self.assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered)
|
||||||
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script src="path/to/script.js"></script>', rendered)
|
||||||
self.assertIn(
|
self.assertInHTML('<script src="path/to/script2.js"></script>', rendered)
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script2.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_bytes(self):
|
def test_bytes(self):
|
||||||
"""
|
"""
|
||||||
|
@ -478,15 +428,8 @@ class MediaPathAsObjectTests(BaseTestCase):
|
||||||
self.assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style3.css" media="print" rel="stylesheet">', rendered)
|
||||||
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="path/to/style4.css" media="screen" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script src="path/to/script.js"></script>', rendered)
|
||||||
self.assertIn(
|
self.assertInHTML('<script src="path/to/script2.js"></script>', rendered)
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;path/to/script2.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_function(self):
|
def test_function(self):
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
|
@ -517,23 +460,10 @@ class MediaPathAsObjectTests(BaseTestCase):
|
||||||
self.assertInHTML('<link href="calendar/style2.css" media="all" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="calendar/style2.css" media="all" rel="stylesheet">', rendered)
|
||||||
self.assertInHTML('<link href="calendar/style3.css" media="all" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="calendar/style3.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script hi src="calendar/script.js"></script>', rendered)
|
||||||
self.assertIn(
|
self.assertInHTML('<script src="calendar/script1.js"></script>', rendered)
|
||||||
"Components.unescapeJs(\\`&lt;script hi src=&quot;calendar/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
self.assertInHTML('<script src="calendar/script2.js"></script>', rendered)
|
||||||
rendered,
|
self.assertInHTML('<script src="calendar/script3.js"></script>', rendered)
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;calendar/script1.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;calendar/script2.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;calendar/script3.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(STATIC_URL="static/")
|
@override_settings(STATIC_URL="static/")
|
||||||
def test_works_with_static(self):
|
def test_works_with_static(self):
|
||||||
|
@ -570,23 +500,10 @@ class MediaPathAsObjectTests(BaseTestCase):
|
||||||
self.assertInHTML('<link href="/static/calendar/style2.css" media="all" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="/static/calendar/style2.css" media="all" rel="stylesheet">', rendered)
|
||||||
self.assertInHTML('<link href="/static/calendar/style3.css" media="all" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="/static/calendar/style3.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script hi src="/static/calendar/script.js"></script>', rendered)
|
||||||
self.assertIn(
|
self.assertInHTML('<script src="/static/calendar/script1.js"></script>', rendered)
|
||||||
"Components.unescapeJs(\\`&lt;script hi src=&quot;/static/calendar/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
self.assertInHTML('<script src="/static/calendar/script2.js"></script>', rendered)
|
||||||
rendered,
|
self.assertInHTML('<script src="/static/calendar/script3.js"></script>', rendered)
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;/static/calendar/script1.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;/static/calendar/script2.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script src=&quot;/static/calendar/script3.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MediaStaticfilesTests(BaseTestCase):
|
class MediaStaticfilesTests(BaseTestCase):
|
||||||
|
@ -634,11 +551,7 @@ class MediaStaticfilesTests(BaseTestCase):
|
||||||
# be searched as specified above (e.g. `calendar/script.js`) inside `static_root` dir.
|
# be searched as specified above (e.g. `calendar/script.js`) inside `static_root` dir.
|
||||||
self.assertInHTML('<link href="/static/calendar/style.css" media="all" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="/static/calendar/style.css" media="all" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script defer src="/static/calendar/script.js"></script>', rendered)
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script defer src=&quot;/static/calendar/script.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
# For context see https://github.com/EmilStenstrom/django-components/issues/522
|
# For context see https://github.com/EmilStenstrom/django-components/issues/522
|
||||||
@override_settings(
|
@override_settings(
|
||||||
|
@ -698,11 +611,7 @@ class MediaStaticfilesTests(BaseTestCase):
|
||||||
'<link href="/static/calendar/style.0eeb72042b59.css" media="all" rel="stylesheet">', rendered
|
'<link href="/static/calendar/style.0eeb72042b59.css" media="all" rel="stylesheet">', rendered
|
||||||
)
|
)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script defer src="/static/calendar/script.e1815e23e0ec.js"></script>', rendered)
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script defer src=&quot;/static/calendar/script.e1815e23e0ec.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MediaRelativePathTests(BaseTestCase):
|
class MediaRelativePathTests(BaseTestCase):
|
||||||
|
@ -790,11 +699,7 @@ class MediaRelativePathTests(BaseTestCase):
|
||||||
rendered,
|
rendered,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<link href="relative_file/relative_file.css" media="all" rel="stylesheet">', rendered)
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;link href=&quot;relative_file/relative_file.css&quot; media=&quot;all&quot; rel=&quot;stylesheet&quot;&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Settings required for autodiscover to work
|
# Settings required for autodiscover to work
|
||||||
@override_settings(
|
@override_settings(
|
||||||
|
@ -863,8 +768,4 @@ class MediaRelativePathTests(BaseTestCase):
|
||||||
self.assertInHTML('<input type="text" name="variable" value="abc">', rendered)
|
self.assertInHTML('<input type="text" name="variable" value="abc">', rendered)
|
||||||
self.assertInHTML('<link href="relative_file_pathobj.css" rel="stylesheet">', rendered)
|
self.assertInHTML('<link href="relative_file_pathobj.css" rel="stylesheet">', rendered)
|
||||||
|
|
||||||
# Command to load the JS from Media.js
|
self.assertInHTML('<script type="module" src="relative_file_pathobj.js"></script>', rendered)
|
||||||
self.assertIn(
|
|
||||||
"Components.unescapeJs(\\`&lt;script type=&quot;module&quot; src=&quot;relative_file_pathobj.js&quot;&gt;&lt;/script&gt;\\`)",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
|
@ -208,7 +208,7 @@ class RenderDependenciesTests(BaseTestCase):
|
||||||
rendered_raw = Template(template_str).render(Context({}))
|
rendered_raw = Template(template_str).render(Context({}))
|
||||||
rendered = render_dependencies(rendered_raw)
|
rendered = render_dependencies(rendered_raw)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("<script"), 3)
|
self.assertEqual(rendered.count("<script"), 4)
|
||||||
self.assertEqual(rendered.count("<style"), 1)
|
self.assertEqual(rendered.count("<style"), 1)
|
||||||
self.assertEqual(rendered.count("<link"), 1)
|
self.assertEqual(rendered.count("<link"), 1)
|
||||||
self.assertEqual(rendered.count("_RENDERED"), 0)
|
self.assertEqual(rendered.count("_RENDERED"), 0)
|
||||||
|
@ -256,7 +256,7 @@ class RenderDependenciesTests(BaseTestCase):
|
||||||
rendered_raw = Template(template_str).render(Context({}))
|
rendered_raw = Template(template_str).render(Context({}))
|
||||||
rendered = render_dependencies(rendered_raw)
|
rendered = render_dependencies(rendered_raw)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("<script"), 3)
|
self.assertEqual(rendered.count("<script"), 4)
|
||||||
self.assertEqual(rendered.count("<style"), 1)
|
self.assertEqual(rendered.count("<style"), 1)
|
||||||
self.assertEqual(rendered.count("<link"), 1)
|
self.assertEqual(rendered.count("<link"), 1)
|
||||||
self.assertEqual(rendered.count("_RENDERED"), 0)
|
self.assertEqual(rendered.count("_RENDERED"), 0)
|
||||||
|
|
|
@ -61,13 +61,22 @@ class DependencyManagerTests(_BaseDepManagerTestCase):
|
||||||
|
|
||||||
keys = await page.evaluate("Object.keys(Components.manager)")
|
keys = await page.evaluate("Object.keys(Components.manager)")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
keys, ["callComponent", "registerComponent", "registerComponentData", "loadScript", "markScriptLoaded"]
|
keys,
|
||||||
|
[
|
||||||
|
"callComponent",
|
||||||
|
"registerComponent",
|
||||||
|
"registerComponentData",
|
||||||
|
"loadJs",
|
||||||
|
"loadCss",
|
||||||
|
"markScriptLoaded",
|
||||||
|
"_loadComponentScripts",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
await page.close()
|
await page.close()
|
||||||
|
|
||||||
|
|
||||||
# Tests for `manager.loadScript()` / `manager.markAsLoaded()`
|
# Tests for `manager.loadJs()` / `manager.loadCss()` / `manager.markAsLoaded()`
|
||||||
@override_settings(STATIC_URL="static/")
|
@override_settings(STATIC_URL="static/")
|
||||||
class LoadScriptTests(_BaseDepManagerTestCase):
|
class LoadScriptTests(_BaseDepManagerTestCase):
|
||||||
@with_playwright
|
@with_playwright
|
||||||
|
@ -81,15 +90,15 @@ class LoadScriptTests(_BaseDepManagerTestCase):
|
||||||
const headBeforeFirstLoad = document.head.innerHTML;
|
const headBeforeFirstLoad = document.head.innerHTML;
|
||||||
|
|
||||||
// Adds a script the first time
|
// Adds a script the first time
|
||||||
manager.loadScript('js', "<script src='/one/two'></script>");
|
manager.loadJs("<script src='/one/two'></script>");
|
||||||
const bodyAfterFirstLoad = document.body.innerHTML;
|
const bodyAfterFirstLoad = document.body.innerHTML;
|
||||||
|
|
||||||
// Does not add it the second time
|
// Does not add it the second time
|
||||||
manager.loadScript('js', "<script src='/one/two'></script>");
|
manager.loadJs("<script src='/one/two'></script>");
|
||||||
const bodyAfterSecondLoad = document.body.innerHTML;
|
const bodyAfterSecondLoad = document.body.innerHTML;
|
||||||
|
|
||||||
// Adds different script
|
// Adds different script
|
||||||
manager.loadScript('js', "<script src='/four/three'></script>");
|
manager.loadJs("<script src='/four/three'></script>");
|
||||||
const bodyAfterThirdLoad = document.body.innerHTML;
|
const bodyAfterThirdLoad = document.body.innerHTML;
|
||||||
|
|
||||||
const headAfterThirdLoad = document.head.innerHTML;
|
const headAfterThirdLoad = document.head.innerHTML;
|
||||||
|
@ -127,15 +136,15 @@ class LoadScriptTests(_BaseDepManagerTestCase):
|
||||||
const bodyBeforeFirstLoad = document.body.innerHTML;
|
const bodyBeforeFirstLoad = document.body.innerHTML;
|
||||||
|
|
||||||
// Adds a script the first time
|
// Adds a script the first time
|
||||||
manager.loadScript('css', "<link href='/one/two'>");
|
manager.loadCss("<link href='/one/two'>");
|
||||||
const headAfterFirstLoad = document.head.innerHTML;
|
const headAfterFirstLoad = document.head.innerHTML;
|
||||||
|
|
||||||
// Does not add it the second time
|
// Does not add it the second time
|
||||||
manager.loadScript('css', "<link herf='/one/two'>");
|
manager.loadCss("<link herf='/one/two'>");
|
||||||
const headAfterSecondLoad = document.head.innerHTML;
|
const headAfterSecondLoad = document.head.innerHTML;
|
||||||
|
|
||||||
// Adds different script
|
// Adds different script
|
||||||
manager.loadScript('css', "<link href='/four/three'>");
|
manager.loadCss("<link href='/four/three'>");
|
||||||
const headAfterThirdLoad = document.head.innerHTML;
|
const headAfterThirdLoad = document.head.innerHTML;
|
||||||
|
|
||||||
const bodyAfterThirdLoad = document.body.innerHTML;
|
const bodyAfterThirdLoad = document.body.innerHTML;
|
||||||
|
@ -172,10 +181,10 @@ class LoadScriptTests(_BaseDepManagerTestCase):
|
||||||
manager.markScriptLoaded('css', '/one/two');
|
manager.markScriptLoaded('css', '/one/two');
|
||||||
manager.markScriptLoaded('js', '/one/three');
|
manager.markScriptLoaded('js', '/one/three');
|
||||||
|
|
||||||
manager.loadScript('css', "<link href='/one/two'>");
|
manager.loadCss("<link href='/one/two'>");
|
||||||
const headAfterFirstLoad = document.head.innerHTML;
|
const headAfterFirstLoad = document.head.innerHTML;
|
||||||
|
|
||||||
manager.loadScript('js', "<script src='/one/three'></script>");
|
manager.loadJs("<script src='/one/three'></script>");
|
||||||
const bodyAfterSecondLoad = document.body.innerHTML;
|
const bodyAfterSecondLoad = document.body.innerHTML;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -127,10 +127,19 @@ class DependencyRenderingTests(BaseTestCase):
|
||||||
self.assertEqual(rendered.count("<link"), 0) # No CSS
|
self.assertEqual(rendered.count("<link"), 0) # No CSS
|
||||||
self.assertEqual(rendered.count("<style"), 0)
|
self.assertEqual(rendered.count("<style"), 0)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("const loadedJsScripts = [];"), 1)
|
# We expect to find this:
|
||||||
self.assertEqual(rendered.count("const loadedCssScripts = [];"), 1)
|
# ```js
|
||||||
self.assertEqual(rendered.count(r"const toLoadJsScripts = [];"), 1)
|
# Components.manager._loadComponentScripts({
|
||||||
self.assertEqual(rendered.count(r"const toLoadCssScripts = [];"), 1)
|
# loadedCssUrls: [],
|
||||||
|
# loadedJsUrls: [],
|
||||||
|
# toLoadCssTags: [],
|
||||||
|
# toLoadJsTags: [],
|
||||||
|
# });
|
||||||
|
# ```
|
||||||
|
self.assertEqual(rendered.count("loadedJsUrls: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("loadedCssUrls: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("toLoadJsTags: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("toLoadCssTags: [],"), 1)
|
||||||
|
|
||||||
def test_no_js_dependencies_when_no_components_used(self):
|
def test_no_js_dependencies_when_no_components_used(self):
|
||||||
registry.register(name="test", component=SimpleComponent)
|
registry.register(name="test", component=SimpleComponent)
|
||||||
|
@ -148,10 +157,19 @@ class DependencyRenderingTests(BaseTestCase):
|
||||||
self.assertEqual(rendered.count("<link"), 0) # No CSS
|
self.assertEqual(rendered.count("<link"), 0) # No CSS
|
||||||
self.assertEqual(rendered.count("<style"), 0)
|
self.assertEqual(rendered.count("<style"), 0)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("const loadedJsScripts = [];"), 1)
|
# We expect to find this:
|
||||||
self.assertEqual(rendered.count("const loadedCssScripts = [];"), 1)
|
# ```js
|
||||||
self.assertEqual(rendered.count(r"const toLoadJsScripts = [];"), 1)
|
# Components.manager._loadComponentScripts({
|
||||||
self.assertEqual(rendered.count(r"const toLoadCssScripts = [];"), 1)
|
# loadedCssUrls: [],
|
||||||
|
# loadedJsUrls: [],
|
||||||
|
# toLoadCssTags: [],
|
||||||
|
# toLoadJsTags: [],
|
||||||
|
# });
|
||||||
|
# ```
|
||||||
|
self.assertEqual(rendered.count("loadedJsUrls: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("loadedCssUrls: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("toLoadJsTags: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("toLoadCssTags: [],"), 1)
|
||||||
|
|
||||||
def test_no_css_dependencies_when_no_components_used(self):
|
def test_no_css_dependencies_when_no_components_used(self):
|
||||||
registry.register(name="test", component=SimpleComponent)
|
registry.register(name="test", component=SimpleComponent)
|
||||||
|
@ -184,22 +202,21 @@ class DependencyRenderingTests(BaseTestCase):
|
||||||
self.assertEqual(rendered.count('<link href="style.css" media="all" rel="stylesheet">'), 1) # Media.css
|
self.assertEqual(rendered.count('<link href="style.css" media="all" rel="stylesheet">'), 1) # Media.css
|
||||||
self.assertEqual(rendered.count("<link"), 1)
|
self.assertEqual(rendered.count("<link"), 1)
|
||||||
self.assertEqual(rendered.count("<style"), 0)
|
self.assertEqual(rendered.count("<style"), 0)
|
||||||
self.assertEqual(rendered.count("<script"), 2)
|
self.assertEqual(rendered.count("<script"), 3)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("const loadedJsScripts = [];"), 1)
|
# We expect to find this:
|
||||||
self.assertEqual(rendered.count("const loadedCssScripts = ["style.css"];"), 1)
|
# ```js
|
||||||
self.assertEqual(
|
# Components.manager._loadComponentScripts({
|
||||||
rendered.count(
|
# loadedCssUrls: ["style.css"],
|
||||||
r"const toLoadJsScripts = [Components.unescapeJs(\`&lt;script src=&quot;script.js&quot;&gt;&lt;/script&gt;\`)];"
|
# loadedJsUrls: ["script.js"],
|
||||||
),
|
# toLoadCssTags: [],
|
||||||
1,
|
# toLoadJsTags: [],
|
||||||
)
|
# });
|
||||||
self.assertEqual(
|
# ```
|
||||||
rendered.count(
|
self.assertEqual(rendered.count("loadedJsUrls: ["script.js"],"), 1)
|
||||||
r"const toLoadCssScripts = [Components.unescapeJs(\`&lt;link href=&quot;style.css&quot; media=&quot;all&quot; rel=&quot;stylesheet&quot;&gt;\`)];"
|
self.assertEqual(rendered.count("loadedCssUrls: ["style.css"],"), 1)
|
||||||
),
|
self.assertEqual(rendered.count("toLoadJsTags: [],"), 1)
|
||||||
1,
|
self.assertEqual(rendered.count("toLoadCssTags: [],"), 1)
|
||||||
)
|
|
||||||
|
|
||||||
def test_single_component_with_dash_or_slash_in_name(self):
|
def test_single_component_with_dash_or_slash_in_name(self):
|
||||||
registry.register(name="te-s/t", component=SimpleComponent)
|
registry.register(name="te-s/t", component=SimpleComponent)
|
||||||
|
@ -219,22 +236,21 @@ class DependencyRenderingTests(BaseTestCase):
|
||||||
self.assertEqual(rendered.count('<link href="style.css" media="all" rel="stylesheet">'), 1) # Media.css
|
self.assertEqual(rendered.count('<link href="style.css" media="all" rel="stylesheet">'), 1) # Media.css
|
||||||
self.assertEqual(rendered.count("<link"), 1)
|
self.assertEqual(rendered.count("<link"), 1)
|
||||||
self.assertEqual(rendered.count("<style"), 0)
|
self.assertEqual(rendered.count("<style"), 0)
|
||||||
self.assertEqual(rendered.count("<script"), 2)
|
self.assertEqual(rendered.count("<script"), 3)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("const loadedJsScripts = [];"), 1)
|
# We expect to find this:
|
||||||
self.assertEqual(rendered.count("const loadedCssScripts = ["style.css"];"), 1)
|
# ```js
|
||||||
self.assertEqual(
|
# Components.manager._loadComponentScripts({
|
||||||
rendered.count(
|
# loadedCssUrls: ["style.css"],
|
||||||
r"const toLoadJsScripts = [Components.unescapeJs(\`&lt;script src=&quot;script.js&quot;&gt;&lt;/script&gt;\`)];"
|
# loadedJsUrls: ["script.js"],
|
||||||
),
|
# toLoadCssTags: [],
|
||||||
1,
|
# toLoadJsTags: [],
|
||||||
)
|
# });
|
||||||
self.assertEqual(
|
# ```
|
||||||
rendered.count(
|
self.assertEqual(rendered.count("loadedJsUrls: ["script.js"],"), 1)
|
||||||
r"const toLoadCssScripts = [Components.unescapeJs(\`&lt;link href=&quot;style.css&quot; media=&quot;all&quot; rel=&quot;stylesheet&quot;&gt;\`)];"
|
self.assertEqual(rendered.count("loadedCssUrls: ["style.css"],"), 1)
|
||||||
),
|
self.assertEqual(rendered.count("toLoadJsTags: [],"), 1)
|
||||||
1,
|
self.assertEqual(rendered.count("toLoadCssTags: [],"), 1)
|
||||||
)
|
|
||||||
|
|
||||||
def test_single_component_placeholder_removed(self):
|
def test_single_component_placeholder_removed(self):
|
||||||
registry.register(name="test", component=SimpleComponent)
|
registry.register(name="test", component=SimpleComponent)
|
||||||
|
@ -285,22 +301,21 @@ class DependencyRenderingTests(BaseTestCase):
|
||||||
# CSS NOT included
|
# CSS NOT included
|
||||||
self.assertEqual(rendered.count("<link"), 0)
|
self.assertEqual(rendered.count("<link"), 0)
|
||||||
self.assertEqual(rendered.count("<style"), 0)
|
self.assertEqual(rendered.count("<style"), 0)
|
||||||
self.assertEqual(rendered.count("<script"), 2)
|
self.assertEqual(rendered.count("<script"), 3)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("const loadedJsScripts = [];"), 1)
|
# We expect to find this:
|
||||||
self.assertEqual(rendered.count("const loadedCssScripts = ["style.css"];"), 1)
|
# ```js
|
||||||
self.assertEqual(
|
# Components.manager._loadComponentScripts({
|
||||||
rendered.count(
|
# loadedCssUrls: ["style.css"],
|
||||||
r"const toLoadJsScripts = [Components.unescapeJs(\`&lt;script src=&quot;script.js&quot;&gt;&lt;/script&gt;\`)];"
|
# loadedJsUrls: ["script.js"],
|
||||||
),
|
# toLoadCssTags: [],
|
||||||
1,
|
# toLoadJsTags: [],
|
||||||
)
|
# });
|
||||||
self.assertEqual(
|
# ```
|
||||||
rendered.count(
|
self.assertEqual(rendered.count("loadedJsUrls: ["script.js"],"), 1)
|
||||||
r"const toLoadCssScripts = [Components.unescapeJs(\`&lt;link href=&quot;style.css&quot; media=&quot;all&quot; rel=&quot;stylesheet&quot;&gt;\`)];"
|
self.assertEqual(rendered.count("loadedCssUrls: ["style.css"],"), 1)
|
||||||
),
|
self.assertEqual(rendered.count("toLoadJsTags: [],"), 1)
|
||||||
1,
|
self.assertEqual(rendered.count("toLoadCssTags: [],"), 1)
|
||||||
)
|
|
||||||
|
|
||||||
def test_all_dependencies_are_rendered_for_component_with_multiple_dependencies(
|
def test_all_dependencies_are_rendered_for_component_with_multiple_dependencies(
|
||||||
self,
|
self,
|
||||||
|
@ -320,31 +335,41 @@ class DependencyRenderingTests(BaseTestCase):
|
||||||
|
|
||||||
self.assertEqual(rendered.count("<link"), 2)
|
self.assertEqual(rendered.count("<link"), 2)
|
||||||
self.assertEqual(rendered.count("<style"), 0)
|
self.assertEqual(rendered.count("<style"), 0)
|
||||||
self.assertEqual(rendered.count("<script"), 2) # Boilerplate scripts
|
self.assertEqual(rendered.count("<script"), 4) # 2 scripts belong to the boilerplate
|
||||||
|
|
||||||
self.assertEqual(rendered.count('<link href="style.css" media="all" rel="stylesheet">'), 1) # Media.css
|
# Media.css
|
||||||
self.assertEqual(rendered.count('<link href="style2.css" media="all" rel="stylesheet">'), 1)
|
self.assertInHTML(
|
||||||
|
"""
|
||||||
self.assertEqual(rendered.count("const loadedJsScripts = [];"), 1)
|
<link href="style.css" media="all" rel="stylesheet">
|
||||||
self.assertEqual(
|
<link href="style2.css" media="all" rel="stylesheet">
|
||||||
rendered.count("const loadedCssScripts = ["style.css", "style2.css"];"), 1
|
""",
|
||||||
|
rendered,
|
||||||
|
count=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
# JS ORDER - "script.js", "script2.js"
|
# Media.js
|
||||||
self.assertEqual(
|
self.assertInHTML(
|
||||||
rendered.count(
|
"""
|
||||||
r"const toLoadJsScripts = [Components.unescapeJs(\`&lt;script src=&quot;script.js&quot;&gt;&lt;/script&gt;\`), Components.unescapeJs(\`&lt;script src=&quot;script2.js&quot;&gt;&lt;/script&gt;\`)];"
|
<script src="script.js"></script>
|
||||||
),
|
<script src="script2.js"></script>
|
||||||
1,
|
""",
|
||||||
|
rendered,
|
||||||
|
count=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
# CSS ORDER - "style.css", "style2.css"
|
# We expect to find this:
|
||||||
self.assertEqual(
|
# ```js
|
||||||
rendered.count(
|
# Components.manager._loadComponentScripts({
|
||||||
r"const toLoadCssScripts = [Components.unescapeJs(\`&lt;link href=&quot;style.css&quot; media=&quot;all&quot; rel=&quot;stylesheet&quot;&gt;\`), Components.unescapeJs(\`&lt;link href=&quot;style2.css&quot; media=&quot;all&quot; rel=&quot;stylesheet&quot;&gt;\`)];"
|
# loadedCssUrls: ["style.css", "style2.css"],
|
||||||
),
|
# loadedJsUrls: ["script.js", "script2.js"],
|
||||||
1,
|
# toLoadCssTags: [],
|
||||||
)
|
# toLoadJsTags: [],
|
||||||
|
# });
|
||||||
|
# ```
|
||||||
|
self.assertEqual(rendered.count("loadedCssUrls: ["style.css", "style2.css"],"), 1)
|
||||||
|
self.assertEqual(rendered.count("loadedJsUrls: ["script.js", "script2.js""), 1)
|
||||||
|
self.assertEqual(rendered.count("toLoadCssTags: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("toLoadJsTags: [],"), 1)
|
||||||
|
|
||||||
def test_no_dependencies_with_multiple_unused_components(self):
|
def test_no_dependencies_with_multiple_unused_components(self):
|
||||||
registry.register(name="inner", component=SimpleComponent)
|
registry.register(name="inner", component=SimpleComponent)
|
||||||
|
@ -361,14 +386,23 @@ class DependencyRenderingTests(BaseTestCase):
|
||||||
# Dependency manager script
|
# Dependency manager script
|
||||||
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
|
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("<script"), 2) # Two 2 scripts belong to the boilerplate
|
self.assertEqual(rendered.count("<script"), 2) # 2 scripts belong to the boilerplate
|
||||||
self.assertEqual(rendered.count("<link"), 0) # No CSS
|
self.assertEqual(rendered.count("<link"), 0) # No CSS
|
||||||
self.assertEqual(rendered.count("<style"), 0)
|
self.assertEqual(rendered.count("<style"), 0)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("const loadedJsScripts = [];"), 1)
|
# We expect to find this:
|
||||||
self.assertEqual(rendered.count("const loadedCssScripts = [];"), 1)
|
# ```js
|
||||||
self.assertEqual(rendered.count("const toLoadJsScripts = [];"), 1)
|
# Components.manager._loadComponentScripts({
|
||||||
self.assertEqual(rendered.count("const toLoadCssScripts = [];"), 1)
|
# loadedCssUrls: [],
|
||||||
|
# loadedJsUrls: [],
|
||||||
|
# toLoadCssTags: [],
|
||||||
|
# toLoadJsTags: [],
|
||||||
|
# });
|
||||||
|
# ```
|
||||||
|
self.assertEqual(rendered.count("loadedJsUrls: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("loadedCssUrls: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("toLoadJsTags: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("toLoadCssTags: [],"), 1)
|
||||||
|
|
||||||
def test_multiple_components_dependencies(self):
|
def test_multiple_components_dependencies(self):
|
||||||
registry.register(name="inner", component=SimpleComponent)
|
registry.register(name="inner", component=SimpleComponent)
|
||||||
|
@ -390,55 +424,76 @@ class DependencyRenderingTests(BaseTestCase):
|
||||||
# NOTE: Should be present only ONCE!
|
# NOTE: Should be present only ONCE!
|
||||||
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
|
self.assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
|
||||||
|
|
||||||
self.assertEqual(rendered.count("<script"), 4) # Two 2 scripts belong to the boilerplate
|
self.assertEqual(rendered.count("<script"), 7) # 2 scripts belong to the boilerplate
|
||||||
self.assertEqual(rendered.count("<link"), 3)
|
self.assertEqual(rendered.count("<link"), 3)
|
||||||
self.assertEqual(rendered.count("<style"), 2)
|
self.assertEqual(rendered.count("<style"), 2)
|
||||||
|
|
||||||
# Components' inlined CSS
|
# Components' inlined CSS
|
||||||
# NOTE: Each of these should be present only ONCE!
|
# NOTE: Each of these should be present only ONCE!
|
||||||
self.assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1)
|
self.assertInHTML(
|
||||||
self.assertInHTML("<style>.my-class { color: red; }</style>", rendered, count=1)
|
"""
|
||||||
|
<style>.my-class { color: red; }</style>
|
||||||
|
<style>.xyz { color: red; }</style>
|
||||||
|
""",
|
||||||
|
rendered,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
# Components' Media.css
|
# Components' Media.css
|
||||||
# NOTE: Each of these should be present only ONCE!
|
# Order:
|
||||||
self.assertInHTML('<link href="xyz1.css" media="all" rel="stylesheet">', rendered, count=1)
|
|
||||||
self.assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1)
|
|
||||||
self.assertInHTML('<link href="style2.css" media="all" rel="stylesheet">', rendered, count=1)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
rendered.count(
|
|
||||||
"const loadedJsScripts = ["/components/cache/OtherComponent_6329ae.js/", "/components/cache/SimpleComponentNested_f02d32.js/"];"
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
rendered.count(
|
|
||||||
"const loadedCssScripts = ["/components/cache/OtherComponent_6329ae.css/", "/components/cache/SimpleComponentNested_f02d32.css/", "style.css", "style2.css", "xyz1.css"];"
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
|
|
||||||
# JS ORDER:
|
|
||||||
# - "script2.js" (from SimpleComponentNested)
|
|
||||||
# - "script.js" (from SimpleComponent inside SimpleComponentNested)
|
|
||||||
# - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested)
|
|
||||||
self.assertEqual(
|
|
||||||
rendered.count(
|
|
||||||
r"const toLoadJsScripts = [Components.unescapeJs(\`&lt;script src=&quot;script2.js&quot;&gt;&lt;/script&gt;\`), Components.unescapeJs(\`&lt;script src=&quot;script.js&quot;&gt;&lt;/script&gt;\`), Components.unescapeJs(\`&lt;script src=&quot;xyz1.js&quot;&gt;&lt;/script&gt;\`)];"
|
|
||||||
),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
|
|
||||||
# CSS ORDER:
|
|
||||||
# - "style.css", "style2.css" (from SimpleComponentNested)
|
# - "style.css", "style2.css" (from SimpleComponentNested)
|
||||||
# - "style.css" (from SimpleComponent inside SimpleComponentNested)
|
# - "style.css" (from SimpleComponent inside SimpleComponentNested)
|
||||||
# - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested)
|
# - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested)
|
||||||
|
self.assertInHTML(
|
||||||
|
"""
|
||||||
|
<link href="style.css" media="all" rel="stylesheet">
|
||||||
|
<link href="style2.css" media="all" rel="stylesheet">
|
||||||
|
<link href="xyz1.css" media="all" rel="stylesheet">
|
||||||
|
""",
|
||||||
|
rendered,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Components' Media.js followed by inlined JS
|
||||||
|
# Order:
|
||||||
|
# - "script2.js" (from SimpleComponentNested)
|
||||||
|
# - "script.js" (from SimpleComponent inside SimpleComponentNested)
|
||||||
|
# - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested)
|
||||||
|
self.assertInHTML(
|
||||||
|
"""
|
||||||
|
<script src="script2.js"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
<script src="xyz1.js"></script>
|
||||||
|
<script>eval(Components.unescapeJs(`console.log("Hello");`))</script>
|
||||||
|
<script>eval(Components.unescapeJs(`console.log("xyz");`))</script>
|
||||||
|
""",
|
||||||
|
rendered,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We expect to find this:
|
||||||
|
# ```js
|
||||||
|
# Components.manager._loadComponentScripts({
|
||||||
|
# loadedCssUrls: ["/components/cache/OtherComponent_6329ae.css/", "/components/cache/SimpleComponentNested_f02d32.css/", "style.css", "style2.css", "xyz1.css"],
|
||||||
|
# loadedJsUrls: ["/components/cache/OtherComponent_6329ae.js/", "/components/cache/SimpleComponentNested_f02d32.js/", "script.js", "script2.js", "xyz1.js"],
|
||||||
|
# toLoadCssTags: [],
|
||||||
|
# toLoadJsTags: [],
|
||||||
|
# });
|
||||||
|
# ```
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
rendered.count(
|
rendered.count(
|
||||||
r"const toLoadCssScripts = [Components.unescapeJs(\`&lt;link href=&quot;style.css&quot; media=&quot;all&quot; rel=&quot;stylesheet&quot;&gt;\`), Components.unescapeJs(\`&lt;link href=&quot;style2.css&quot; media=&quot;all&quot; rel=&quot;stylesheet&quot;&gt;\`), Components.unescapeJs(\`&lt;link href=&quot;xyz1.css&quot; media=&quot;all&quot; rel=&quot;stylesheet&quot;&gt;\`)];"
|
"loadedJsUrls: ["/components/cache/OtherComponent_6329ae.js/", "/components/cache/SimpleComponentNested_f02d32.js/", "script.js", "script2.js", "xyz1.js"],"
|
||||||
),
|
),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
rendered.count(
|
||||||
|
"loadedCssUrls: ["/components/cache/OtherComponent_6329ae.css/", "/components/cache/SimpleComponentNested_f02d32.css/", "style.css", "style2.css", "xyz1.css"],"
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(rendered.count("toLoadJsTags: [],"), 1)
|
||||||
|
self.assertEqual(rendered.count("toLoadCssTags: [],"), 1)
|
||||||
|
|
||||||
def test_multiple_components_all_placeholders_removed(self):
|
def test_multiple_components_all_placeholders_removed(self):
|
||||||
registry.register(name="inner", component=SimpleComponent)
|
registry.register(name="inner", component=SimpleComponent)
|
||||||
|
|
|
@ -215,3 +215,132 @@ class E2eDependencyRenderingTests(BaseTestCase):
|
||||||
self.assertEqual("rgb(255, 0, 0)", data["myStyle2Color"]) # AKA 'color: red'
|
self.assertEqual("rgb(255, 0, 0)", data["myStyle2Color"]) # AKA 'color: red'
|
||||||
|
|
||||||
await page.close()
|
await page.close()
|
||||||
|
|
||||||
|
@with_playwright
|
||||||
|
async def test_js_executed_in_order__js(self):
|
||||||
|
single_comp_url = TEST_SERVER_URL + "/js-order/js"
|
||||||
|
|
||||||
|
page: Page = await self.browser.new_page()
|
||||||
|
await page.goto(single_comp_url)
|
||||||
|
|
||||||
|
test_js: types.js = """() => {
|
||||||
|
// NOTE: This variable should be defined by `check_script_order` component,
|
||||||
|
// and it should contain all other variables defined by the previous components
|
||||||
|
return checkVars;
|
||||||
|
}"""
|
||||||
|
|
||||||
|
data = await page.evaluate(test_js)
|
||||||
|
|
||||||
|
# Check components' inlined JS got loaded
|
||||||
|
self.assertEqual(data["testSimpleComponent"], "kapowww!")
|
||||||
|
self.assertEqual(data["testSimpleComponentNested"], "bongo!")
|
||||||
|
self.assertEqual(data["testOtherComponent"], "wowzee!")
|
||||||
|
|
||||||
|
# Check JS from Media.js got loaded
|
||||||
|
self.assertEqual(data["testMsg"], {"hello": "world"})
|
||||||
|
self.assertEqual(data["testMsg2"], {"hello2": "world2"})
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
@with_playwright
|
||||||
|
async def test_js_executed_in_order__media(self):
|
||||||
|
single_comp_url = TEST_SERVER_URL + "/js-order/media"
|
||||||
|
|
||||||
|
page: Page = await self.browser.new_page()
|
||||||
|
await page.goto(single_comp_url)
|
||||||
|
|
||||||
|
test_js: types.js = """() => {
|
||||||
|
// NOTE: This variable should be defined by `check_script_order` component,
|
||||||
|
// and it should contain all other variables defined by the previous components
|
||||||
|
return checkVars;
|
||||||
|
}"""
|
||||||
|
|
||||||
|
data = await page.evaluate(test_js)
|
||||||
|
|
||||||
|
# Check components' inlined JS got loaded
|
||||||
|
# NOTE: The Media JS are loaded BEFORE the components' JS, so they should be empty
|
||||||
|
self.assertEqual(data["testSimpleComponent"], None)
|
||||||
|
self.assertEqual(data["testSimpleComponentNested"], None)
|
||||||
|
self.assertEqual(data["testOtherComponent"], None)
|
||||||
|
|
||||||
|
# Check JS from Media.js
|
||||||
|
self.assertEqual(data["testMsg"], {"hello": "world"})
|
||||||
|
self.assertEqual(data["testMsg2"], {"hello2": "world2"})
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
# In this case the component whose JS is accessing data from other components
|
||||||
|
# is used in the template before the other components. So the JS should
|
||||||
|
# not be able to access the data from the other components.
|
||||||
|
@with_playwright
|
||||||
|
async def test_js_executed_in_order__invalid(self):
|
||||||
|
single_comp_url = TEST_SERVER_URL + "/js-order/invalid"
|
||||||
|
|
||||||
|
page: Page = await self.browser.new_page()
|
||||||
|
await page.goto(single_comp_url)
|
||||||
|
|
||||||
|
test_js: types.js = """() => {
|
||||||
|
// checkVars was defined BEFORE other components, so it should be empty!
|
||||||
|
return checkVars;
|
||||||
|
}"""
|
||||||
|
|
||||||
|
data = await page.evaluate(test_js)
|
||||||
|
|
||||||
|
# Check components' inlined JS got loaded
|
||||||
|
self.assertEqual(data["testSimpleComponent"], None)
|
||||||
|
self.assertEqual(data["testSimpleComponentNested"], None)
|
||||||
|
self.assertEqual(data["testOtherComponent"], None)
|
||||||
|
|
||||||
|
# Check JS from Media.js got loaded
|
||||||
|
self.assertEqual(data["testMsg"], None)
|
||||||
|
self.assertEqual(data["testMsg2"], None)
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
@with_playwright
|
||||||
|
async def test_alpine__head(self):
|
||||||
|
single_comp_url = TEST_SERVER_URL + "/alpine/head"
|
||||||
|
|
||||||
|
page: Page = await self.browser.new_page()
|
||||||
|
await page.goto(single_comp_url)
|
||||||
|
|
||||||
|
component_text = await page.locator('[x-data="alpine_test"]').text_content()
|
||||||
|
self.assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123")
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
@with_playwright
|
||||||
|
async def test_alpine__body(self):
|
||||||
|
single_comp_url = TEST_SERVER_URL + "/alpine/body"
|
||||||
|
|
||||||
|
page: Page = await self.browser.new_page()
|
||||||
|
await page.goto(single_comp_url)
|
||||||
|
|
||||||
|
component_text = await page.locator('[x-data="alpine_test"]').text_content()
|
||||||
|
self.assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123")
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
@with_playwright
|
||||||
|
async def test_alpine__body2(self):
|
||||||
|
single_comp_url = TEST_SERVER_URL + "/alpine/body2"
|
||||||
|
|
||||||
|
page: Page = await self.browser.new_page()
|
||||||
|
await page.goto(single_comp_url)
|
||||||
|
|
||||||
|
component_text = await page.locator('[x-data="alpine_test"]').text_content()
|
||||||
|
self.assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123")
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
@with_playwright
|
||||||
|
async def test_alpine__invalid(self):
|
||||||
|
single_comp_url = TEST_SERVER_URL + "/alpine/invalid"
|
||||||
|
|
||||||
|
page: Page = await self.browser.new_page()
|
||||||
|
await page.goto(single_comp_url)
|
||||||
|
|
||||||
|
component_text = await page.locator('[x-data="alpine_test"]').text_content()
|
||||||
|
self.assertHTMLEqual(component_text.strip(), "ALPINE_TEST:")
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue