Files
rendercv/tests/renderer/templater/test_connections.py

420 lines
14 KiB
Python

from typing import Literal, get_args
import pydantic
import pytest
from rendercv.renderer.templater.connections import (
compute_connections,
compute_connections_for_markdown,
compute_connections_for_typst,
fontawesome_icons,
parse_connections,
)
from rendercv.schema.models.cv.custom_connection import CustomConnection
from rendercv.schema.models.cv.cv import Cv
from rendercv.schema.models.cv.social_network import SocialNetwork, SocialNetworkName
from rendercv.schema.models.design.classic_theme import (
ClassicTheme,
Connections,
Header,
)
from rendercv.schema.models.locale.locale import EnglishLocale
from rendercv.schema.models.rendercv_model import RenderCVModel
def create_cv(
*,
key_order: list[str],
email: list[str] | str | None = None,
phone: list[str] | str | None = None,
website: list[str] | str | None = None,
location: str | None = None,
social_networks: list[SocialNetwork] | None = None,
custom_connections: list[CustomConnection] | None = None,
) -> Cv:
"""Create a test CV with specified connection fields in a specific order.
The key_order parameter determines the order connections appear in the CV,
which is preserved in the final output.
"""
cv_data = {}
for key in key_order:
if key == "email" and email is not None:
cv_data["email"] = email
elif key == "phone" and phone is not None:
cv_data["phone"] = phone
elif key == "website" and website is not None:
cv_data["website"] = website
elif key == "location" and location is not None:
cv_data["location"] = location
elif key == "social_networks" and social_networks is not None:
cv_data["social_networks"] = social_networks
elif key == "custom_connections" and custom_connections is not None:
cv_data["custom_connections"] = custom_connections
cv_data["name"] = "John Doe"
if social_networks is not None:
cv_data["social_networks"] = social_networks
if custom_connections is not None:
cv_data["custom_connections"] = custom_connections
return Cv.model_validate(cv_data)
def create_rendercv_model(
cv: Cv,
*,
use_icons: bool = True,
make_links: bool = True,
phone_format: Literal["national", "international", "E164"] = "international",
) -> RenderCVModel:
"""Create a test RenderCVModel with specified connection formatting options.
Used to test different connection rendering configurations (icons, links, phone format).
"""
return RenderCVModel(
cv=cv,
design=ClassicTheme(
header=Header(
connections=Connections(
show_icons=use_icons,
hyperlink=make_links,
phone_number_format=phone_format,
)
)
),
locale=EnglishLocale(),
)
class TestParseConnections:
@pytest.mark.parametrize(
("field", "value", "expected_count", "expected_icon"),
[
("email", "john@example.com", 1, fontawesome_icons["email"]),
(
"email",
["john@example.com", "jane@example.com"],
2,
fontawesome_icons["email"],
),
("phone", "+14155552671", 1, fontawesome_icons["phone"]),
("phone", ["+14155552671", "+442071234567"], 2, fontawesome_icons["phone"]),
("website", "https://example.com", 1, fontawesome_icons["website"]),
(
"website",
["https://example.com", "https://blog.example.com"],
2,
fontawesome_icons["website"],
),
("location", "New York, NY", 1, fontawesome_icons["location"]),
],
)
def test_parse_single_field_type(self, field, value, expected_count, expected_icon):
cv = create_cv(key_order=[field], **{field: value})
model = create_rendercv_model(cv)
connections = parse_connections(model)
assert len(connections) == expected_count
assert all(c.fontawesome_icon == expected_icon for c in connections)
def test_email_connection_structure(self):
cv = create_cv(key_order=["email"], email="john@example.com")
model = create_rendercv_model(cv)
connections = parse_connections(model)
assert connections[0].url == "mailto:john@example.com"
assert connections[0].body == "john@example.com"
@pytest.mark.parametrize(
("phone", "phone_format", "expected_body"),
[
("+14155552671", "international", "+1 415-555-2671"),
("+14155552671", "national", "(415) 555-2671"),
("+14155552671", "E164", "+14155552671"),
],
)
def test_phone_formatting(self, phone, phone_format, expected_body):
cv = create_cv(key_order=["phone"], phone=phone)
model = create_rendercv_model(cv, phone_format=phone_format)
connections = parse_connections(model)
assert connections[0].body == expected_body
def test_website_connection_structure(self):
cv = create_cv(key_order=["website"], website="https://example.com")
model = create_rendercv_model(cv)
connections = parse_connections(model)
# Pydantic's HttpUrl adds trailing slash
assert connections[0].url == "https://example.com/"
assert connections[0].body == "example.com"
def test_location_has_no_url(self):
cv = create_cv(key_order=["location"], location="New York, NY")
model = create_rendercv_model(cv)
connections = parse_connections(model)
assert connections[0].url is None
assert connections[0].body == "New York, NY"
def test_social_network_connection(self):
social = SocialNetwork(network="LinkedIn", username="johndoe")
cv = create_cv(key_order=[], social_networks=[social])
model = create_rendercv_model(cv)
connections = parse_connections(model)
assert len(connections) == 1
assert connections[0].fontawesome_icon == fontawesome_icons["LinkedIn"]
assert connections[0].url == "https://linkedin.com/in/johndoe"
assert connections[0].body == "johndoe"
def test_key_order_is_preserved(self):
cv = create_cv(
key_order=["location", "email", "phone", "website"],
location="NYC",
email="john@example.com",
phone="+14155552671",
website="https://example.com",
)
model = create_rendercv_model(cv)
connections = parse_connections(model)
icons = [c.fontawesome_icon for c in connections]
assert icons == [
fontawesome_icons["location"],
fontawesome_icons["email"],
fontawesome_icons["phone"],
fontawesome_icons["website"],
]
def test_social_networks_appended_after_key_order(self):
cv = create_cv(
key_order=["email"],
email="john@example.com",
social_networks=[
SocialNetwork(network="LinkedIn", username="johndoe"),
SocialNetwork(network="GitHub", username="johndoe"),
],
)
model = create_rendercv_model(cv)
connections = parse_connections(model)
icons = [c.fontawesome_icon for c in connections]
assert icons == [
fontawesome_icons["email"],
fontawesome_icons["LinkedIn"],
fontawesome_icons["GitHub"],
]
def test_empty_key_order_returns_empty_list(self):
cv = create_cv(key_order=[])
model = create_rendercv_model(cv)
connections = parse_connections(model)
assert connections == []
def test_unknown_keys_are_ignored(self):
cv = create_cv(
key_order=["unknown_field", "email", "another_unknown"],
email="john@example.com",
)
model = create_rendercv_model(cv)
connections = parse_connections(model)
assert len(connections) == 1
assert connections[0].fontawesome_icon == fontawesome_icons["email"]
def test_custom_connections_use_placeholder_and_icon(self):
custom_connection = CustomConnection(
fontawesome_icon="calendar-days",
placeholder="Book a call",
url=pydantic.HttpUrl("https://cal.com/johndoe"),
)
cv = create_cv(
key_order=["custom_connections"],
custom_connections=[custom_connection],
)
model = create_rendercv_model(cv)
connections = parse_connections(model)
assert len(connections) == 1
connection = connections[0]
assert connection.fontawesome_icon == "calendar-days"
assert connection.url == str(custom_connection.url)
assert connection.body == "Book a call"
def test_custom_connection_without_url_is_plain(self):
custom_connection = CustomConnection(
fontawesome_icon="calendar-days",
placeholder="Office Hours",
url=None,
)
cv = create_cv(
key_order=["custom_connections"],
custom_connections=[custom_connection],
)
model = create_rendercv_model(cv)
connections = parse_connections(model)
assert len(connections) == 1
assert connections[0].url is None
assert connections[0].body == "Office Hours"
class TestComputeConnectionsForTypst:
@pytest.mark.parametrize(
("use_icons", "make_links", "expected_patterns"),
[
(True, True, ["#connection-with-icon", "#link("]),
(True, False, ["#connection-with-icon"]),
(False, True, ["#link("]),
(False, False, []),
],
)
def test_icon_and_link_combinations(self, use_icons, make_links, expected_patterns):
cv = create_cv(key_order=["email"], email="john@example.com")
model = create_rendercv_model(cv, use_icons=use_icons, make_links=make_links)
result = compute_connections_for_typst(model)
assert len(result) == 1
for pattern in expected_patterns:
assert pattern in result[0]
# Check that unwanted patterns are NOT present
if not use_icons:
assert "#connection-with-icon" not in result[0]
if not make_links:
assert "#link(" not in result[0]
def test_connection_without_url_has_no_link(self):
cv = create_cv(key_order=["location"], location="New York, NY")
model = create_rendercv_model(cv, use_icons=True, make_links=True)
result = compute_connections_for_typst(model)
assert "#connection-with-icon" in result[0]
assert "#link(" not in result[0]
def test_plain_text_output_when_no_formatting(self):
cv = create_cv(key_order=["email"], email="john@example.com")
model = create_rendercv_model(cv, use_icons=False, make_links=False)
result = compute_connections_for_typst(model)
assert "john\\@example.com" in result[0]
def test_custom_connection_without_url_is_not_hyperlinked(self):
custom_connection = CustomConnection(
fontawesome_icon="calendar-days",
placeholder="Office Hours",
url=None,
)
cv = create_cv(
key_order=["custom_connections"],
custom_connections=[custom_connection],
)
model = create_rendercv_model(cv, use_icons=True, make_links=True)
result = compute_connections_for_typst(model)
assert "#link(" not in result[0]
assert '#connection-with-icon("calendar-days")' in result[0]
assert "Office Hours" in result[0]
class TestComputeConnectionsForMarkdown:
def test_connection_with_url_formatted_as_markdown_link(self):
cv = create_cv(key_order=["email"], email="john@example.com")
model = create_rendercv_model(cv)
result = compute_connections_for_markdown(model)
assert result[0] == "[john@example.com](mailto:john@example.com)"
def test_connection_without_url_is_plain_text(self):
cv = create_cv(key_order=["location"], location="New York, NY")
model = create_rendercv_model(cv)
result = compute_connections_for_markdown(model)
assert result[0] == "New York, NY"
assert "[" not in result[0]
def test_custom_connection_renders_markdown_link_when_url_is_present(self):
custom_connection = CustomConnection(
fontawesome_icon="calendar-days",
placeholder="Book a call",
url=pydantic.HttpUrl("https://cal.com/johndoe"),
)
cv = create_cv(
key_order=["custom_connections"],
custom_connections=[custom_connection],
)
model = create_rendercv_model(cv)
result = compute_connections_for_markdown(model)
expected_url = str(custom_connection.url)
assert result[0] == f"[Book a call]({expected_url})"
def test_custom_connection_without_url_is_plain_text(self):
custom_connection = CustomConnection(
fontawesome_icon="calendar-days",
placeholder="Office Hours",
url=None,
)
cv = create_cv(
key_order=["custom_connections"],
custom_connections=[custom_connection],
)
model = create_rendercv_model(cv)
result = compute_connections_for_markdown(model)
assert result[0] == "Office Hours"
class TestComputeConnections:
@pytest.mark.parametrize("file_type", ["typst", "markdown"])
def test_dispatches_to_correct_formatter(self, file_type):
cv = create_cv(key_order=["email"], email="john@example.com")
model = create_rendercv_model(cv, use_icons=True, make_links=True)
result = compute_connections(model, file_type)
assert len(result) == 1
assert isinstance(result[0], str)
if file_type == "typst":
assert "#connection-with-icon" in result[0]
else: # markdown
assert result[0].startswith("[")
class TestIconMapping:
@pytest.mark.parametrize("network", get_args(SocialNetworkName.__value__))
def test_all_social_networks_have_icons(self, network):
assert network in fontawesome_icons, (
f"Missing icon for social network: {network}"
)
@pytest.mark.parametrize("conn_type", ["email", "phone", "website", "location"])
def test_all_connection_types_have_icons(self, conn_type):
assert conn_type in fontawesome_icons, (
f"Missing icon for connection type: {conn_type}"
)