449 lines
21 KiB
C#
449 lines
21 KiB
C#
using JobTrackerApi.Data;
|
||
using JobTrackerApi.Models;
|
||
using JobTrackerApi.Services;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Moq;
|
||
using Xunit;
|
||
|
||
namespace JobTrackerApi.Tests;
|
||
|
||
public sealed class CvBuilderTests
|
||
{
|
||
private static StructuredCvProfile Rich() => new()
|
||
{
|
||
Contact = { FullName = "Ada Lovelace", Headline = "Engineer", Email = "ada@example.com", Location = "Oslo" },
|
||
Summary = { "Builder of engines." },
|
||
Jobs =
|
||
{
|
||
new StructuredCvJob { Id = "job1", Title = "Senior Eng", Company = "Acme", Location = "Oslo", Start = "Jan 2020", End = "Present", IsCurrent = true, Bullets = { "Built X" }, Skills = { "C#" } },
|
||
new StructuredCvJob { Id = "job2", Title = "Eng", Company = "Beta", Start = "2018", End = "2019", Bullets = { "Built Y" } },
|
||
},
|
||
Education = { new StructuredCvEducation { Id = "ed1", Qualification = "BSc", Institution = "Uni" } },
|
||
Skills = { "C#", "SQL" },
|
||
Projects = { new StructuredCvProject { Id = "pr1", Name = "Proj", Role = "Lead", Skills = { "React" } } },
|
||
Certifications = { new StructuredCvCertification { Id = "c1", Name = "AZ-204", Issuer = "MS" } },
|
||
Languages = { new StructuredCvLanguage { Name = "English", Level = "Native" } },
|
||
};
|
||
|
||
// ---- Resolver (the lens over the master profile) ----
|
||
|
||
[Fact]
|
||
public void Resolver_defaults_include_every_populated_section()
|
||
{
|
||
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings(), "Fallback", null);
|
||
Assert.Equal("Ada Lovelace", model.FullName);
|
||
var keys = model.Sections.Select(s => s.Key).ToList();
|
||
Assert.Contains("summary", keys);
|
||
Assert.Contains("experience", keys);
|
||
Assert.Contains("skills", keys);
|
||
Assert.Contains("languages", keys);
|
||
}
|
||
|
||
[Fact]
|
||
public void Resolver_hides_a_section_when_configured()
|
||
{
|
||
var settings = new CvVariantSettings { Sections = { new CvSectionSetting { Key = "skills", Hidden = true } } };
|
||
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
|
||
Assert.DoesNotContain("skills", model.Sections.Select(s => s.Key));
|
||
// Other sections still render (appended after the explicit order).
|
||
Assert.Contains("experience", model.Sections.Select(s => s.Key));
|
||
}
|
||
|
||
[Fact]
|
||
public void Resolver_respects_explicit_section_order()
|
||
{
|
||
var settings = new CvVariantSettings
|
||
{
|
||
Sections = { new CvSectionSetting { Key = "skills" }, new CvSectionSetting { Key = "summary" } },
|
||
};
|
||
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
|
||
var keys = model.Sections.Select(s => s.Key).ToList();
|
||
Assert.True(keys.IndexOf("skills") < keys.IndexOf("summary"));
|
||
}
|
||
|
||
[Fact]
|
||
public void Resolver_applies_item_override_bullets_without_touching_the_master()
|
||
{
|
||
var profile = Rich();
|
||
var settings = new CvVariantSettings();
|
||
settings.Overrides["job1"] = new CvItemOverride { Bullets = new() { "Tailored bullet" } };
|
||
var model = CvVariantResolver.Build(profile, settings, "F", null);
|
||
var exp = model.Sections.First(s => s.Key == "experience");
|
||
Assert.Equal(new[] { "Tailored bullet" }, exp.Entries[0].Bullets);
|
||
// Master profile is untouched — the override is a lens, not a write.
|
||
Assert.Equal(new[] { "Built X" }, profile.Jobs[0].Bullets);
|
||
}
|
||
|
||
[Fact]
|
||
public void Resolver_hides_a_single_item_by_key()
|
||
{
|
||
var settings = new CvVariantSettings();
|
||
settings.Overrides["job2"] = new CvItemOverride { Hidden = true };
|
||
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
|
||
var exp = model.Sections.First(s => s.Key == "experience");
|
||
Assert.Single(exp.Entries);
|
||
Assert.Equal("Senior Eng", exp.Entries[0].Title);
|
||
}
|
||
|
||
[Fact]
|
||
public void Resolver_adds_variant_only_custom_sections()
|
||
{
|
||
var settings = new CvVariantSettings
|
||
{
|
||
CustomSections = { new CvCustomSectionSetting { Key = "vol", Title = "Volunteering", Items = { "Coached juniors" } } },
|
||
};
|
||
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
|
||
Assert.Contains(model.Sections, s => s.Title == "Volunteering" && s.Bullets.Contains("Coached juniors"));
|
||
}
|
||
|
||
[Fact]
|
||
public void Resolver_places_custom_sections_in_the_shared_section_order()
|
||
{
|
||
var settings = new CvVariantSettings
|
||
{
|
||
Sections =
|
||
{
|
||
new CvSectionSetting { Key = "custom:vol" },
|
||
new CvSectionSetting { Key = "summary" },
|
||
},
|
||
CustomSections =
|
||
{
|
||
new CvCustomSectionSetting { Key = "vol", Title = "Volunteering", Items = { "Coached juniors" } },
|
||
},
|
||
};
|
||
|
||
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
|
||
Assert.True(model.Sections.FindIndex(section => section.Key == "custom:vol")
|
||
< model.Sections.FindIndex(section => section.Key == "summary"));
|
||
}
|
||
|
||
// ---- Renderer (one path, every theme is data) ----
|
||
|
||
[Fact]
|
||
public void Every_catalog_theme_renders_valid_html()
|
||
{
|
||
var renderer = new ThemedCvRenderer();
|
||
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings { ShowPhoto = true }, "F", "data:image/png;base64,AAAA");
|
||
foreach (var theme in CvThemeCatalog.Themes)
|
||
{
|
||
var result = renderer.Render(model, theme, new CvVariantSettings { ThemeId = theme.Id });
|
||
Assert.Contains("<html", result.Html);
|
||
Assert.Contains("Ada Lovelace", result.Html);
|
||
Assert.Contains("Senior Eng", result.Html);
|
||
Assert.Equal(theme.Id, result.ThemeId);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void Two_column_theme_emits_a_sidebar_single_does_not()
|
||
{
|
||
var renderer = new ThemedCvRenderer();
|
||
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings(), "F", null);
|
||
var technical = renderer.Render(model, CvThemeCatalog.Resolve("technical"), new CvVariantSettings { ThemeId = "technical" });
|
||
var minimal = renderer.Render(model, CvThemeCatalog.Resolve("minimal"), new CvVariantSettings { ThemeId = "minimal" });
|
||
Assert.Contains("class=\"sidebar\"", technical.Html);
|
||
Assert.DoesNotContain("class=\"sidebar\"", minimal.Html);
|
||
}
|
||
|
||
[Fact]
|
||
public void Bullets_support_safe_inline_markdown_and_escape_everything_else()
|
||
{
|
||
var renderer = new ThemedCvRenderer();
|
||
var profile = new StructuredCvProfile { Summary = { "**bold** and *italic* and __under__ and [site](https://x.io) <script>alert(1)</script>" } };
|
||
var model = CvVariantResolver.Build(profile, new CvVariantSettings(), "F", null);
|
||
var html = renderer.Render(model, CvThemeCatalog.Resolve("modern"), new CvVariantSettings { ThemeId = "modern" }).Html;
|
||
Assert.Contains("<strong>bold</strong>", html);
|
||
Assert.Contains("<em>italic</em>", html);
|
||
Assert.Contains("<u>under</u>", html);
|
||
Assert.Contains("<a href=\"https://x.io\">site</a>", html);
|
||
Assert.Contains("<script>", html); // the real tag is escaped, inert
|
||
Assert.DoesNotContain("<script>", html);
|
||
}
|
||
|
||
[Fact]
|
||
public void Item_order_reorders_entries_without_touching_the_master()
|
||
{
|
||
var profile = Rich(); // jobs: job1 (Senior Eng), job2 (Eng)
|
||
var settings = new CvVariantSettings
|
||
{
|
||
Sections = { new CvSectionSetting { Key = "experience", ItemOrder = new() { "job2", "job1" } } },
|
||
};
|
||
var model = CvVariantResolver.Build(profile, settings, "F", null);
|
||
var exp = model.Sections.First(s => s.Key == "experience");
|
||
Assert.Equal(new[] { "Eng", "Senior Eng" }, exp.Entries.Select(e => e.Title));
|
||
Assert.Equal(new[] { "Senior Eng", "Eng" }, profile.Jobs.Select(j => j.Title)); // master untouched
|
||
}
|
||
|
||
[Fact]
|
||
public void Rendered_output_has_print_break_rules_and_themes_expose_ats_flag()
|
||
{
|
||
var renderer = new ThemedCvRenderer();
|
||
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings(), "F", null);
|
||
var html = renderer.Render(model, CvThemeCatalog.Resolve("ats-classic"), new CvVariantSettings { ThemeId = "ats-classic" }).Html;
|
||
Assert.Contains("break-inside:avoid", html);
|
||
Assert.True(CvThemeCatalog.Resolve("ats-classic").AtsFriendly);
|
||
Assert.False(CvThemeCatalog.Resolve("technical").AtsFriendly); // sidebar = not ATS-safe
|
||
}
|
||
|
||
[Fact]
|
||
public void Long_content_wraps_and_can_flow_across_pages_without_shrinking_typography()
|
||
{
|
||
var profile = Rich();
|
||
profile.Contact.FullName = new string('N', 180);
|
||
profile.Contact.Email = $"{new string('e', 180)}@example.com";
|
||
profile.Jobs[0].Title = new string('T', 220);
|
||
profile.Jobs[0].Company = new string('C', 220);
|
||
profile.Jobs[0].Bullets = Enumerable.Range(1, 7)
|
||
.Select(index => index == 1 ? new string('x', 500) : $"Detailed achievement {index} with readable typography.")
|
||
.ToList();
|
||
|
||
var renderer = new ThemedCvRenderer();
|
||
var model = CvVariantResolver.Build(profile, new CvVariantSettings(), "F", null);
|
||
var html = renderer.Render(model, CvThemeCatalog.Resolve("technical"), new CvVariantSettings { ThemeId = "technical" }).Html;
|
||
|
||
Assert.Contains("class=\"entry entry-flow\"", html);
|
||
Assert.Contains("class=\"item-flow\"", html);
|
||
Assert.Contains("overflow-wrap:anywhere", html);
|
||
Assert.Contains("grid-template-columns:62mm minmax(0,1fr)", html);
|
||
Assert.Contains("overflow:visible", html);
|
||
Assert.Contains("white-space:normal", html);
|
||
Assert.DoesNotContain("transform:scale", html);
|
||
}
|
||
|
||
[Fact]
|
||
public void Header_band_and_sidebar_contact_text_own_their_contrasting_palette()
|
||
{
|
||
var renderer = new ThemedCvRenderer();
|
||
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings(), "F", null);
|
||
var modern = renderer.Render(model, CvThemeCatalog.Resolve("modern"), new CvVariantSettings { ThemeId = "modern" }).Html;
|
||
var lightAccent = renderer.Render(model, CvThemeCatalog.Resolve("modern"), new CvVariantSettings { ThemeId = "modern", AccentColor = "#f8fafc" }).Html;
|
||
var technicalTheme = CvThemeCatalog.Resolve("technical");
|
||
var technical = renderer.Render(model, technicalTheme, new CvVariantSettings { ThemeId = "technical" }).Html;
|
||
|
||
Assert.Contains(".header-band .name,.header-band .headline,.header-band .contact,.header-band .contact-item,.header-band a{color:#fff;}", modern);
|
||
Assert.Contains(".header-band .name,.header-band .headline,.header-band .contact,.header-band .contact-item,.header-band a{color:#000;}", lightAccent);
|
||
Assert.Contains($".sidebar .contact,.sidebar .headline,.sidebar .entry-meta,.sidebar .entry-subtitle{{color:{technicalTheme.SidebarInk};}}", technical);
|
||
}
|
||
|
||
[Fact]
|
||
public void Variant_settings_reject_css_injection_in_visual_overrides()
|
||
{
|
||
var settings = CvVariantSettingsJson.Normalize(new CvVariantSettings
|
||
{
|
||
AccentColor = "#fff;} </style><script>alert(1)</script>",
|
||
HeadingFont = "Arial;}</style><script>alert(2)</script>",
|
||
BodyFont = "'Segoe UI', Roboto, Arial, sans-serif",
|
||
});
|
||
|
||
Assert.Null(settings.AccentColor);
|
||
Assert.Null(settings.HeadingFont);
|
||
Assert.Equal("'Segoe UI', Roboto, Arial, sans-serif", settings.BodyFont);
|
||
}
|
||
|
||
[Fact]
|
||
public void Accent_override_reaches_the_css()
|
||
{
|
||
var renderer = new ThemedCvRenderer();
|
||
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings(), "F", null);
|
||
var result = renderer.Render(model, CvThemeCatalog.Resolve("modern"), new CvVariantSettings { ThemeId = "modern", AccentColor = "#123456" });
|
||
Assert.Contains("#123456", result.Html);
|
||
}
|
||
|
||
[Fact]
|
||
public void Design_and_layout_overrides_are_normalized_and_reach_the_shared_renderer()
|
||
{
|
||
var settings = CvVariantSettingsJson.Normalize(new CvVariantSettings
|
||
{
|
||
ThemeId = "modern",
|
||
Layout = "sidebar-right",
|
||
TextColor = "#123456",
|
||
MutedColor = "#456789",
|
||
BackgroundColor = "#fafafa",
|
||
BaseFontSizePt = 99,
|
||
HeadingSizePt = 15,
|
||
LineHeight = 1.55,
|
||
PageMarginMm = 20,
|
||
SidebarWidthMm = 70,
|
||
SidebarSections = new() { "skills", "languages" },
|
||
SkillsStyle = "text",
|
||
});
|
||
var model = CvVariantResolver.Build(Rich(), settings, "F", null);
|
||
var html = new ThemedCvRenderer().Render(model, CvThemeCatalog.Resolve("modern"), settings).Html;
|
||
|
||
Assert.Equal(13, settings.BaseFontSizePt);
|
||
Assert.Contains("grid-template-columns:minmax(0,1fr) 70mm", html);
|
||
Assert.Contains("color:#123456", html);
|
||
Assert.Contains("background:#fafafa", html);
|
||
Assert.Contains("line-height:1.55", html);
|
||
Assert.Contains("font-size:13pt", html);
|
||
Assert.Contains("class=\"skills-text\">C# · SQL</p>", html);
|
||
}
|
||
|
||
[Fact]
|
||
public void Norwegian_variant_localizes_default_labels_dates_and_keeps_custom_links_clickable()
|
||
{
|
||
var profile = Rich();
|
||
profile.Jobs[0].StartDate = "2020-01";
|
||
profile.Contact.GitHub = "github.com/ada";
|
||
profile.Contact.Links.Add(new StructuredCvLink { Label = "Portfolio", Url = "https://ada.example/work" });
|
||
var settings = new CvVariantSettings { Language = "no", DateFormat = "year" };
|
||
|
||
var model = CvVariantResolver.Build(profile, settings, "F", null);
|
||
var html = new ThemedCvRenderer().Render(model, CvThemeCatalog.Resolve("modern"), settings).Html;
|
||
|
||
Assert.Equal("Arbeidserfaring", model.Sections.First(section => section.Key == "experience").Title);
|
||
Assert.Equal("2020 – nå", model.Sections.First(section => section.Key == "experience").Entries[0].Meta);
|
||
Assert.Contains("href=\"https://github.com/ada\"", html);
|
||
Assert.Contains("href=\"https://ada.example/work\">Portfolio</a>", html);
|
||
}
|
||
|
||
// ---- Variant service (CRUD, autosave history, public, render) ----
|
||
|
||
private static (JobTrackerContext db, CvVariantService svc) NewService(string userId, string? dbName = null)
|
||
{
|
||
var options = new DbContextOptionsBuilder<JobTrackerContext>()
|
||
.UseInMemoryDatabase(dbName ?? Guid.NewGuid().ToString())
|
||
.Options;
|
||
var currentUser = new Mock<ICurrentUserService>();
|
||
currentUser.SetupGet(s => s.UserId).Returns(userId);
|
||
var db = new JobTrackerContext(options, currentUser.Object);
|
||
var svc = new CvVariantService(db, new CareerProfileService(db), new ThemedCvRenderer());
|
||
return (db, svc);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Create_then_save_increments_version_and_keeps_history()
|
||
{
|
||
var (db, svc) = NewService("user-1");
|
||
await using var _ = db;
|
||
var created = await svc.CreateAsync("user-1", "My CV", null, new CvVariantSettings { ThemeId = "modern" }, default);
|
||
Assert.Equal(1, created.Version);
|
||
|
||
var saved = await svc.SaveAsync("user-1", created.Id, "My CV v2", new CvVariantSettings { ThemeId = "nordic" }, "autosave", default);
|
||
Assert.Equal(2, saved!.Version);
|
||
|
||
var versions = await svc.ListVersionsAsync("user-1", created.Id, default);
|
||
Assert.Equal(new[] { 2, 1 }, versions.Select(v => v.Version));
|
||
Assert.True(versions[0].IsCurrent);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Restore_reapplies_old_settings_as_a_new_version()
|
||
{
|
||
var (db, svc) = NewService("user-1");
|
||
await using var _ = db;
|
||
var v = await svc.CreateAsync("user-1", "CV", null, new CvVariantSettings { ThemeId = "modern" }, default);
|
||
await svc.SaveAsync("user-1", v.Id, null, new CvVariantSettings { ThemeId = "creative" }, "autosave", default);
|
||
|
||
var restored = await svc.RestoreVersionAsync("user-1", v.Id, 1, default);
|
||
Assert.Equal(3, restored!.Version);
|
||
Assert.Equal("modern", CvVariantSettingsJson.Deserialize(restored.SettingsJson).ThemeId);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Duplicate_copies_settings_with_a_fresh_slug()
|
||
{
|
||
var (db, svc) = NewService("user-1");
|
||
await using var _ = db;
|
||
var v = await svc.CreateAsync("user-1", "CV", null, new CvVariantSettings { ThemeId = "elegant" }, default);
|
||
var copy = await svc.DuplicateAsync("user-1", v.Id, null, default);
|
||
Assert.NotEqual(v.PublicSlug, copy!.PublicSlug);
|
||
Assert.Equal("elegant", CvVariantSettingsJson.Deserialize(copy.SettingsJson).ThemeId);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Create_rejects_a_job_application_owned_by_another_user()
|
||
{
|
||
var (db, svc) = NewService("user-1");
|
||
await using var _ = db;
|
||
var company = new Company { OwnerUserId = "user-2", Name = "Other tenant" };
|
||
var foreignJob = new JobApplication { OwnerUserId = "user-2", JobTitle = "Private role", Company = company };
|
||
db.Add(foreignJob);
|
||
await db.SaveChangesAsync();
|
||
|
||
await Assert.ThrowsAsync<ArgumentException>(() =>
|
||
svc.CreateAsync("user-1", "CV", foreignJob.Id, new CvVariantSettings(), default));
|
||
Assert.Empty(await db.CvVariants.IgnoreQueryFilters().ToListAsync());
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Delete_removes_the_variant_and_its_versions()
|
||
{
|
||
var (db, svc) = NewService("user-1");
|
||
await using var _ = db;
|
||
var v = await svc.CreateAsync("user-1", "CV", null, null, default);
|
||
Assert.True(await svc.DeleteAsync("user-1", v.Id, default));
|
||
Assert.Null(await svc.GetAsync("user-1", v.Id, default));
|
||
Assert.Empty(await db.CvVariantVersions.IgnoreQueryFilters().Where(x => x.CvVariantId == v.Id).ToListAsync());
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Render_pulls_content_from_the_master_profile()
|
||
{
|
||
var (db, svc) = NewService("user-1");
|
||
await using var _ = db;
|
||
await new CareerProfileService(db).SaveVersionAsync("user-1", Rich(), "manual", default);
|
||
var v = await svc.CreateAsync("user-1", "CV", null, new CvVariantSettings { ThemeId = "modern" }, default);
|
||
|
||
var render = await svc.RenderAsync("user-1", v.Id, new CvRenderPerson("Fallback", null), default);
|
||
Assert.NotNull(render);
|
||
Assert.Contains("Ada Lovelace", render!.Html);
|
||
Assert.Contains("Senior Eng", render.Html);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Public_render_bypasses_the_tenant_filter()
|
||
{
|
||
var dbName = Guid.NewGuid().ToString();
|
||
var (ownerDb, ownerSvc) = NewService("user-1", dbName);
|
||
await new CareerProfileService(ownerDb).SaveVersionAsync("user-1", Rich(), "manual", default);
|
||
var v = await ownerSvc.CreateAsync("user-1", "CV", null, new CvVariantSettings { ThemeId = "minimal" }, default);
|
||
await ownerSvc.SetPublicAsync("user-1", v.Id, true, default);
|
||
await ownerDb.DisposeAsync();
|
||
|
||
// Anonymous context: no current user, so the tenant filter denies everything by default.
|
||
var (anonDb, anonSvc) = NewService(null!, dbName);
|
||
await using var _ = anonDb;
|
||
var owner = await anonSvc.GetPublicOwnerAsync(v.PublicSlug, default);
|
||
Assert.Equal("user-1", owner);
|
||
var result = await anonSvc.RenderPublicAsync(v.PublicSlug, new CvRenderPerson("Ada Lovelace", null), default);
|
||
Assert.NotNull(result);
|
||
Assert.Contains("Ada Lovelace", result!.Value.render.Html);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Private_variant_is_not_served_publicly()
|
||
{
|
||
var (db, svc) = NewService("user-1");
|
||
await using var _ = db;
|
||
var v = await svc.CreateAsync("user-1", "CV", null, null, default);
|
||
Assert.Null(await svc.GetPublicOwnerAsync(v.PublicSlug, default));
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Re_enabling_public_access_rotates_the_revoked_link()
|
||
{
|
||
var (db, svc) = NewService("user-1");
|
||
await using var _ = db;
|
||
var v = await svc.CreateAsync("user-1", "CV", null, null, default);
|
||
|
||
await svc.SetPublicAsync("user-1", v.Id, true, default);
|
||
var revokedSlug = v.PublicSlug;
|
||
await svc.SetPublicAsync("user-1", v.Id, false, default);
|
||
await svc.SetPublicAsync("user-1", v.Id, true, default);
|
||
|
||
Assert.NotEqual(revokedSlug, v.PublicSlug);
|
||
Assert.Null(await svc.GetPublicOwnerAsync(revokedSlug, default));
|
||
Assert.Equal("user-1", await svc.GetPublicOwnerAsync(v.PublicSlug, default));
|
||
}
|
||
[Fact]
|
||
public void Premium_theme_policy_keeps_three_free_themes_available()
|
||
{
|
||
Assert.Equal(3, CvThemeCatalog.Themes.Count(t => CvThemeCatalog.CanUse(t.Id, false)));
|
||
Assert.Equal(5, CvThemeCatalog.Themes.Count(t => t.Premium));
|
||
Assert.All(CvThemeCatalog.Themes, t => Assert.True(CvThemeCatalog.CanUse(t.Id, true)));
|
||
Assert.False(CvThemeCatalog.CanUse("unknown", true));
|
||
}
|
||
|
||
}
|