"""Extraction coverage for EJS (Embedded JavaScript) templates (#3811, #2511).
A `.ejs` file is HTML carrying `<% %>` scriptlets and inline `\n",
)
result = extract_ejs(tpl)
assert result.get("parse_errors") is None
assert "boot()" in _labels(result)
def test_extract_ejs_require_and_import_resolve_to_real_files(tmp_path):
tpl = _write(
tmp_path / "views" / "page.ejs",
"<%\n"
"const helpers = require('../lib/helpers');\n"
"import { format } from '../lib/format';\n"
"%>\n"
"
<%= format(x) %>
\n",
)
helpers = _write(tmp_path / "lib" / "helpers.js", "exports.h = {};\n")
fmt = _write(tmp_path / "lib" / "format.ts", "export function format() {}\n")
result = extract_ejs(tpl)
targets = {e["target_file"] for e in result["edges"] if e.get("target_file")}
assert str(helpers) in targets
assert str(fmt) in targets
def test_extract_ejs_include_resolves_to_the_real_partial(tmp_path):
"""`<%- include('partials/header') %>` links the view to the partial file.
This is the relationship an Express view graph is built on (#2511). The
include is spelled relative to the view without a leading `./` (as in the
issue's example), and must resolve to the actual `.ejs` file on disk.
"""
tpl = _write(
tmp_path / "views" / "inbox.ejs",
"<%- include('partials/header') %>\n"
"body
\n",
)
partial = _write(
tmp_path / "views" / "partials" / "header.ejs",
"\n",
)
result = extract_ejs(tpl)
includes = _edges(result, "includes")
assert len(includes) == 1
assert includes[0]["target"] == _make_id(str(partial))
assert includes[0]["target_file"] == str(partial)
assert includes[0]["source_location"] == "L1"
def test_extract_ejs_end_to_end_links_partial_file_node(tmp_path):
"""Through `extract` the stamped include edge repoints at the partial's node."""
tpl = _write(
tmp_path / "views" / "inbox.ejs",
"<%- include('partials/header') %>\n",
)
partial = _write(
tmp_path / "views" / "partials" / "header.ejs",
"\n",
)
result = extract([tpl, partial], cache_root=tmp_path, root=tmp_path)
labels = {n["id"]: n["label"] for n in result["nodes"]}
targets = {labels.get(e["target"]) for e in _edges(result, "includes")}
assert "header.ejs" in targets
def test_extract_ejs_unresolved_include_keeps_a_stub(tmp_path):
"""A views-root/bare include that resolves to no file still links something."""
tpl = _write(
tmp_path / "views" / "inbox.ejs",
"<%- include('layouts/app', { title: 'Inbox' }) %>\n",
)
result = extract_ejs(tpl)
includes = _edges(result, "includes")
assert len(includes) == 1
labels = {n["id"]: n["label"] for n in result["nodes"]}
assert labels[includes[0]["target"]] == "layouts/app"
def test_extract_ejs_dynamic_import_in_scriptlet(tmp_path):
tpl = _write(
tmp_path / "views" / "lazy.ejs",
"<% import('./chart.js').then(function(m) { m.init(); }); %>\n",
)
chart = _write(tmp_path / "views" / "chart.js", "export function init() {}\n")
result = extract_ejs(tpl)
dynamic = _edges(result, "dynamic_import")
assert len(dynamic) == 1
assert dynamic[0]["target_file"] == str(chart)
def test_extract_ejs_comment_and_escape_tags_are_not_code(tmp_path):
"""`<%# … %>` is a comment and `<%%` a literal `<%`; neither is a tag."""
tpl = _write(
tmp_path / "views" / "quirks.ejs",
"<%# a comment %>\n"
"literal <%% not a tag %>
\n"
"<% function real() { return 1; } %>\n",
)
result = extract_ejs(tpl)
assert result.get("parse_errors") is None
labels = _labels(result)
assert "real()" in labels
# The escaped literal must not have started a code tag.
assert not any("not a tag" in label for label in labels)
def test_extract_ejs_markup_only_does_not_crash(tmp_path):
tpl = _write(tmp_path / "views" / "plain.ejs", "no script here
\n")
result = extract_ejs(tpl)
assert isinstance(result, dict)
assert result.get("parse_errors") is None
assert _edges(result, "includes") == []
def test_extract_ejs_unreadable_file_reports_error(tmp_path):
result = extract_ejs(tmp_path / "missing" / "Gone.ejs")
assert result["nodes"] == [] and result["edges"] == []
assert "error" in result
def test_extract_ejs_no_longer_reports_missing_extractor(tmp_path, capsys):
"""The #1689 "no AST extractor" warning must be gone now that `.ejs` is wired."""
tpl = _write(
tmp_path / "views" / "page.ejs",
"<% function f() { return 1; } %>\n<%= f() %>
\n",
)
result = extract([tpl], cache_root=tmp_path, root=tmp_path)
err = capsys.readouterr().err
assert "no AST extractor" not in err
assert any(n.get("label") == "page.ejs" for n in result["nodes"])