fix: preserve plugin details card order
This commit is contained in:
parent
6375f1f67e
commit
eea9a3e410
@ -14,6 +14,72 @@ public class PluginDetailsUITests(ITestOutputHelper output) : PageTest
|
||||
{
|
||||
private readonly XUnitLogger _log = new("PluginDetailsUITests", output);
|
||||
|
||||
[Fact]
|
||||
public async Task PluginDetails_NonEmbed_KeepsReviewsStackedUnderDescription()
|
||||
{
|
||||
await using var tester = new PlaywrightTester(_log);
|
||||
tester.Server.ReuseDatabase = false;
|
||||
await tester.StartAsync();
|
||||
|
||||
var ownerId = await tester.Server.CreateFakeUserAsync("layout-owner@x.com", confirmEmail: true, githubVerified: true);
|
||||
const string slug = "plugin-details-layout";
|
||||
await tester.Server.CreateAndBuildPluginAsync(ownerId, slug);
|
||||
|
||||
await tester.Page!.SetViewportSizeAsync(1600, 1000);
|
||||
await tester.GoToUrl($"/public/plugins/{slug}");
|
||||
await tester.AssertNoError();
|
||||
|
||||
var descriptionCard = tester.Page.Locator(".card").Filter(new LocatorFilterOptions { HasText = "Description" });
|
||||
var detailsCard = tester.Page.Locator("#download-btn").Locator("xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' card ')][1]");
|
||||
var reviewsCard = tester.Page.Locator("#reviews");
|
||||
|
||||
await Expect(descriptionCard).ToHaveCountAsync(1);
|
||||
await Expect(detailsCard).ToHaveCountAsync(1);
|
||||
await Expect(reviewsCard).ToHaveCountAsync(1);
|
||||
|
||||
var descriptionBox = await descriptionCard.BoundingBoxAsync() ?? throw new InvalidOperationException("Description card was not visible.");
|
||||
var detailsBox = await detailsCard.BoundingBoxAsync() ?? throw new InvalidOperationException("Details card was not visible.");
|
||||
var reviewsBox = await reviewsCard.BoundingBoxAsync() ?? throw new InvalidOperationException("Reviews card was not visible.");
|
||||
|
||||
var reviewsGap = reviewsBox.Y - (descriptionBox.Y + descriptionBox.Height);
|
||||
Assert.InRange(reviewsGap, 0, 80);
|
||||
Assert.True(
|
||||
reviewsBox.Y < detailsBox.Y + detailsBox.Height,
|
||||
"Reviews should stack under the description column instead of waiting for the metadata card height.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PluginDetails_EmbedNarrow_KeepsVersionBeforeReviews()
|
||||
{
|
||||
await using var tester = new PlaywrightTester(_log);
|
||||
tester.Server.ReuseDatabase = false;
|
||||
await tester.StartAsync();
|
||||
|
||||
var ownerId = await tester.Server.CreateFakeUserAsync("embed-layout-owner@x.com", confirmEmail: true, githubVerified: true);
|
||||
const string slug = "plugin-details-embed-layout";
|
||||
await tester.Server.CreateAndBuildPluginAsync(ownerId, slug);
|
||||
|
||||
await tester.Page!.SetViewportSizeAsync(700, 1000);
|
||||
await tester.GoToUrl($"/public/plugins/{slug}?embed=1");
|
||||
await tester.AssertNoError();
|
||||
|
||||
var descriptionCard = tester.Page.Locator(".plugin-details-description-card");
|
||||
var detailsCard = tester.Page.Locator(".plugin-details-metadata-card");
|
||||
var reviewsCard = tester.Page.Locator(".plugin-details-reviews-card");
|
||||
|
||||
await Expect(descriptionCard).ToHaveCountAsync(1);
|
||||
await Expect(detailsCard).ToHaveCountAsync(1);
|
||||
await Expect(reviewsCard).ToHaveCountAsync(1);
|
||||
await Expect(tester.Page.Locator("#btcpay-install-plugin-btn")).ToHaveCountAsync(1);
|
||||
|
||||
var descriptionBox = await descriptionCard.BoundingBoxAsync() ?? throw new InvalidOperationException("Description card was not visible.");
|
||||
var detailsBox = await detailsCard.BoundingBoxAsync() ?? throw new InvalidOperationException("Details card was not visible.");
|
||||
var reviewsBox = await reviewsCard.BoundingBoxAsync() ?? throw new InvalidOperationException("Reviews card was not visible.");
|
||||
|
||||
Assert.True(descriptionBox.Y < detailsBox.Y, "Description should stay above metadata in embedded details.");
|
||||
Assert.True(detailsBox.Y < reviewsBox.Y, "Metadata should stay above reviews in embedded details.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PluginDetails_Reviews_Flow_Works()
|
||||
{
|
||||
|
||||
@ -60,6 +60,18 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.EmbedMode)
|
||||
{
|
||||
<style>
|
||||
@@media (max-width: 991.98px) {
|
||||
/* Let cards from both columns share the same Bootstrap order in narrow embeds. */
|
||||
body.embed-mode .plugin-details-main > .plugin-details-column {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="@containerClass"
|
||||
data-embed-page="details"
|
||||
data-plugin-slug="@Model.Plugin.ProjectSlug"
|
||||
@ -100,123 +112,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card">
|
||||
<div class="row plugin-details-main @(Model.EmbedMode ? "gx-0 gx-lg-4" : null)">
|
||||
<div class="col-12 col-lg-8 plugin-details-column">
|
||||
<div class="card plugin-details-description-card @(Model.EmbedMode ? "col-12 order-1" : null)">
|
||||
<div class="card-body">
|
||||
<h4>Description</h4>
|
||||
<p>@Model.Plugin.Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-4 mt-4 mt-lg-0">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-6 d-flex align-items-center"><strong>Version</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<div class="dropdown d-inline-block">
|
||||
<button id="version-dropdown-btn" class="btn btn-secondary btn-sm dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@string.Join(".", Model.Plugin.Version.Split('.').Take(3))
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" style="max-height: 140px; overflow-y: auto;">
|
||||
@foreach (var v in Model.PluginVersions)
|
||||
{
|
||||
var shortV = string.Join(".", v.Split('.').Take(3));
|
||||
<li>
|
||||
<a class="dropdown-item @(v == Model.Plugin.Version ? "active" : "")"
|
||||
href="#" data-version="@v" data-display="@shortV">@shortV</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<input type="hidden" id="version-selector" name="Plugin.Version" value="@Model.Plugin.Version" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>Published</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<span id="plugin-published-date">@(buildDate != default ? buildDate.ToString("MMM dd, yyyy") : "")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="plugin-repository-row" class="row mb-3 @(string.IsNullOrEmpty(sourceUrl) ? "d-none" : "")">
|
||||
<div class="col-6"><strong>Repository</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<a id="plugin-source-url" href="@sourceUrl" target="_blank" rel="noopener noreferrer">View Source</a>
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.Plugin.Documentation))
|
||||
{
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>Documentation</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<a href="@Model.Plugin.Documentation" target="_blank" rel="noopener noreferrer">View Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div id="plugin-min-btcpay-row" class="row mb-3 @(string.IsNullOrEmpty(Model.Plugin.BTCPayMinVersion) ? "d-none" : "")">
|
||||
<div class="col-6"><strong>Min. BTCPay Version</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<span id="plugin-min-btcpay-version">@Model.Plugin.BTCPayMinVersion</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="plugin-max-btcpay-row" class="row mb-3 @(string.IsNullOrEmpty(Model.Plugin.BTCPayMaxVersion) ? "d-none" : "")">
|
||||
<div class="col-6"><strong>Max. BTCPay Version</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<span id="plugin-max-btcpay-version">@Model.Plugin.BTCPayMaxVersion</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (dependencies != null)
|
||||
{
|
||||
foreach (var dep in dependencies)
|
||||
{
|
||||
if (string.Equals(dep["Identifier"]?.ToString(), "BTCPayServer", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>@dep["Identifier"]</strong></div>
|
||||
<div class="col-6 text-end">@dep["Condition"]</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>Created</strong></div>
|
||||
<div class="col-6 text-end">@Model.Plugin.CreatedDate.ToString("MMM dd, yyyy")</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>Rating</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<span class="d-inline-flex align-items-center gap-1 lh-1">
|
||||
<span class="fw-semibold">@((Model.Plugin.RatingSummary?.Average ?? 0m).ToString("0.0"))</span>
|
||||
<span class="text-warning"><vc:icon symbol="star-fill" /></span>
|
||||
<span class="text-muted small">(@(Model.Plugin.RatingSummary?.TotalReviews ?? 0))</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (!Model.EmbedMode)
|
||||
{
|
||||
<a id="download-btn"
|
||||
asp-action="Download" asp-controller="Api"
|
||||
asp-route-pluginSlug="@Model.Plugin.ProjectSlug"
|
||||
asp-route-version="@Model.Plugin.Version"
|
||||
class="btn btn-primary w-100" target="_blank" rel="noopener noreferrer">Download</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button"
|
||||
id="btcpay-install-plugin-btn"
|
||||
class="btn btn-primary w-100"
|
||||
data-plugin-install
|
||||
disabled="@(string.IsNullOrWhiteSpace(pluginIdentifier) ? "disabled" : null)">Install in BTCPay Server</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8">
|
||||
@if (mediaItems.Any())
|
||||
{
|
||||
<div class="card mt-4">
|
||||
<div class="card mt-4 @(Model.EmbedMode ? "col-12 order-3" : null)">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-3">Media</h4>
|
||||
<div id="plugin-media-carousel" class="plugin-media-carousel" tabindex="0" aria-label="Plugin media carousel">
|
||||
@ -280,7 +187,7 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card mt-4" id="reviews">
|
||||
<div class="card mt-4 plugin-details-reviews-card @(Model.EmbedMode ? "col-12 order-4" : null)" id="reviews">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="mb-0">Reviews</h4>
|
||||
@ -488,10 +395,111 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="col-12 col-lg-4 mt-4 mt-lg-0 plugin-details-column">
|
||||
<div class="card plugin-details-metadata-card @(Model.EmbedMode ? "col-12 order-2 mt-4 mt-lg-0" : null)">
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-6 d-flex align-items-center"><strong>Version</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<div class="dropdown d-inline-block">
|
||||
<button id="version-dropdown-btn" class="btn btn-secondary btn-sm dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
@string.Join(".", Model.Plugin.Version.Split('.').Take(3))
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" style="max-height: 140px; overflow-y: auto;">
|
||||
@foreach (var v in Model.PluginVersions)
|
||||
{
|
||||
var shortV = string.Join(".", v.Split('.').Take(3));
|
||||
<li>
|
||||
<a class="dropdown-item @(v == Model.Plugin.Version ? "active" : "")"
|
||||
href="#" data-version="@v" data-display="@shortV">@shortV</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<input type="hidden" id="version-selector" name="Plugin.Version" value="@Model.Plugin.Version" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>Published</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<span id="plugin-published-date">@(buildDate != default ? buildDate.ToString("MMM dd, yyyy") : "")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="plugin-repository-row" class="row mb-3 @(string.IsNullOrEmpty(sourceUrl) ? "d-none" : "")">
|
||||
<div class="col-6"><strong>Repository</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<a id="plugin-source-url" href="@sourceUrl" target="_blank" rel="noopener noreferrer">View Source</a>
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.Plugin.Documentation))
|
||||
{
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>Documentation</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<a href="@Model.Plugin.Documentation" target="_blank" rel="noopener noreferrer">View Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div id="plugin-min-btcpay-row" class="row mb-3 @(string.IsNullOrEmpty(Model.Plugin.BTCPayMinVersion) ? "d-none" : "")">
|
||||
<div class="col-6"><strong>Min. BTCPay Version</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<span id="plugin-min-btcpay-version">@Model.Plugin.BTCPayMinVersion</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="plugin-max-btcpay-row" class="row mb-3 @(string.IsNullOrEmpty(Model.Plugin.BTCPayMaxVersion) ? "d-none" : "")">
|
||||
<div class="col-6"><strong>Max. BTCPay Version</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<span id="plugin-max-btcpay-version">@Model.Plugin.BTCPayMaxVersion</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (dependencies != null)
|
||||
{
|
||||
foreach (var dep in dependencies)
|
||||
{
|
||||
if (string.Equals(dep["Identifier"]?.ToString(), "BTCPayServer", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>@dep["Identifier"]</strong></div>
|
||||
<div class="col-6 text-end">@dep["Condition"]</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>Created</strong></div>
|
||||
<div class="col-6 text-end">@Model.Plugin.CreatedDate.ToString("MMM dd, yyyy")</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-6"><strong>Rating</strong></div>
|
||||
<div class="col-6 text-end">
|
||||
<span class="d-inline-flex align-items-center gap-1 lh-1">
|
||||
<span class="fw-semibold">@((Model.Plugin.RatingSummary?.Average ?? 0m).ToString("0.0"))</span>
|
||||
<span class="text-warning"><vc:icon symbol="star-fill" /></span>
|
||||
<span class="text-muted small">(@(Model.Plugin.RatingSummary?.TotalReviews ?? 0))</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (!Model.EmbedMode)
|
||||
{
|
||||
<a id="download-btn"
|
||||
asp-action="Download" asp-controller="Api"
|
||||
asp-route-pluginSlug="@Model.Plugin.ProjectSlug"
|
||||
asp-route-version="@Model.Plugin.Version"
|
||||
class="btn btn-primary w-100" target="_blank" rel="noopener noreferrer">Download</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button"
|
||||
id="btcpay-install-plugin-btn"
|
||||
class="btn btn-primary w-100"
|
||||
data-plugin-install
|
||||
disabled="@(string.IsNullOrWhiteSpace(pluginIdentifier) ? "disabled" : null)">Install in BTCPay Server</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Contributors?.Any() == true)
|
||||
{
|
||||
<div class="card mt-4">
|
||||
<div class="card mt-4 @(Model.EmbedMode ? "col-12 order-5" : null)">
|
||||
<div class="card-body">
|
||||
<h4>Contributors</h4>
|
||||
<div class="row">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user