"""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", "
hi
\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", "
hi
\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"])