Skip to content

Commit

Permalink
[Playground] Add Events components (#311)
Browse files Browse the repository at this point in the history
* Implement EventItemComponent

* Implement EventListComponent

* Add EventList page

* Fix flex align problem and replace nested div to FluentCard

* Add CSS configurations on EventItemComponent

* Adjust padding of EventListComponent

* Add comment on EventList page

* Rename EventList to Events

* Integrate model, Add Id, Alignments

* Add progress ring during loading in EventListComponent

* Use parameter Id to FluentGrid's Id

* Implement test methods for Event components

* Show a message when there are no events user joined

* Adjust EventItemComponent padding

* Add tests for Events page in AppHost testing project

* Revert launchSettings.json

* Remove redundant references, Make properties nullable

* Remove SetParametersAsync at EventItemComponent

* Re-order methods by access modifiers priority

* Revert launchSettings.json with the final new line

* Add async to NavigateToEventDetails

* Sort entities in code behind by common .NET coding convention

* Rename events-list to event-list

* Refactor no-events card with FluentCard

* Replace event title element to FluentLabel

* Implement event link with FluentNavLink

* Relocate OnInitializedAsync

* Remove progress ring

* Make color follow Fluent UI neutral layer color

* Add id attributes on sub-elements in event cards

* Rename _events to events, Add getting up to 4 events prior to the showing loop

* Remove redundant null initialization in EventListComponent

* Integrate linq code to foreach

* Integrate no-events item to EventItemComponent and adjust breakpoints

* Remove breakpoint parameter, update grid's justification to center

* Adjust breakpoint for desktop size

* Fix FluentCard's class name

* Add new tests dedicated to EventItemComponent

* Update grid's justify to FlexStart

* Remove EventListComponent Id

* Add HasNoEvent parameter for EventItemComponent

* Move grid's style code to a dedicated CSS file

* Restore EventListComponent's Id parameter and change its parameter name value

* Separate CSS code for EventItemComponent to its own CSS file

* Refactor EventsPageTests for CSS isolation work

* Prune redundant CSS configurations

* Integrate more CSS properties

* Add title in Events component page

* Adjust margin properties to prevent hiding the focus square
  • Loading branch information
Capella87 authored Oct 10, 2024
1 parent e702672 commit bbdeeb1
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 0 deletions.
12 changes: 12 additions & 0 deletions src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Events.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@page "/events"

<PageTitle>Events</PageTitle>

<h1>Your Events</h1>

<p>This component demonstrates showing up to 4 events that user joined.</p>

<EventListComponent Id="user-event-list" @rendermode="InteractiveServer" />

@code {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@if (HasNoEvent)
{
<FluentGridItem Class="event-list-item" Id="@Id" xs="12" lg="12" xxl="12" Justify="JustifyContent.Center" Style="text-align: center;">
<div class="event-item-flex-wrapper">
<FluentCard Class="no-event" MinimalStyle="true">
<FluentLabel Typo="Typography.Header">You dont have any events you have participated in now.</FluentLabel>
</FluentCard>
</div>
</FluentGridItem>
}
else
{
<FluentGridItem Class="event-list-item" Id="@Id" xs="12" lg="6" xxl="3" Justify="JustifyContent.Center" Style="text-align: center;">
<div class="event-item-flex-wrapper">
<FluentCard Class="event" MinimalStyle="true">
<FluentNavLink Class="event-details-link" Href="@($"/events/{Id}")" >
<FluentLabel Class="event-title" Typo="Typography.PaneHeader" Style="margin-block: 0.5em;">
@Title
</FluentLabel>
</FluentNavLink>
<div class="event-summary card border">
<div class="card-body">
<h5>@Summary</h5>
</div>
</div>
</FluentCard>
</div>
</FluentGridItem>
}





@code {
[Parameter]
public string? Id { get; set; }

[Parameter]
public string? Title { get; set; }

[Parameter]
public string? Summary { get; set; }

[Parameter]
public bool HasNoEvent { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
::deep .event {
display: flex;
padding: 0em;
flex-direction: column;
align-items: normal;
align-content: center;
justify-content: center;
}

::deep .no-event {
display: flex;
padding: 0em;
flex-direction: row;
align-items: center;
align-content: center;
justify-content: center;
background-color: transparent;
box-shadow: none !important;
border: hidden;
}

::deep .event-details-link {
flex-grow: 0;
align-self: center;
margin-block: 0.3em;
}

div.event-summary.card.border {
background-color: var(--neutral-layer-2);
padding: 1em;
padding-inline: 1.5em;
flex-grow: 1;
}

div.event-item-flex-wrapper {
flex-grow: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
@using AzureOpenAIProxy.PlaygroundApp.Models;

<!-- Source: https://khalidabuhakmeh.com/blazors-css-isolation-deep-issue-and-solution -->
<div>
<FluentGrid AdaptiveRendering="true"
Id="@Id"
Spacing="3"
Justify="JustifyContent.FlexStart"
Class="event-list">
@if (events == null || events.Any() == false)
{
<EventItemComponent Id="no-event-item" HasNoEvent="true"></EventItemComponent>
}
else
{
// Shows up to 4 events that the user currently joined.
@foreach (var e in events.Take(4))
{
<EventItemComponent Id="@e.EventId.ToString()"
Title="@e.Title"
Summary="@e.Summary">
</EventItemComponent>
}
}
</FluentGrid>
</div>

@code {
private List<EventDetails>? events;

[Parameter]
public string? Id { get; set; }

protected override async Task OnInitializedAsync()
{
// TODO: Fetch events from the API server.
events = await CreateEventDetailsAsync();
}

private async Task<List<EventDetails>> CreateEventDetailsAsync()
{
return await Task.FromResult(new List<EventDetails>
{
new EventDetails
{
EventId = Guid.NewGuid(),
Title = "Event 1",
Summary = "Summary 1",
MaxTokenCap = 1000,
DailyRequestCap = 100
},
new EventDetails
{
EventId = Guid.NewGuid(),
Title = "Event 2",
Summary = "Et iusto clita ipsum et. Amet lorem est lorem takimata et aliquyam. Aliquyam invidunt dolor erat eu sed ut sadipscing justo sed justo amet magna ea lorem ipsum exerci. Erat diam tempor imperdiet lorem duis. Amet est sanctus tempor kasd erat odio diam accumsan stet. Voluptua aliquyam magna at no vulputate justo labore labore eos stet. Dolore ut ad sadipscing sit elitr ipsum commodo nam invidunt wisi labore vero feugait sanctus sea ad et sadipscing. Possim tempor nonummy erat et no erat lorem in dolore consequat eos feugiat justo vero. Ut eirmod et duis accusam dolore est sea duis dolor et duis illum. Esse ut aliquyam placerat enim amet et labore sadipscing sed stet duo eos at consequat autem accusam lorem invidunt. Sea clita rebum eum et no dolore et sit. Liber aliquyam duo eu. Feugiat sadipscing sed eos sanctus gubergren dolore amet. Erat liber nam ea aliquam ut autem dolores magna aliquyam illum vero vulputate ut accusam est rebum. Et takimata est dolore ut elitr gubergren sanctus ipsum magna magna at sed amet dolores amet. Rebum dolore sit ea et gubergren. Dolore aliquam ipsum in at est justo justo ipsum. Ipsum nisl sea lorem.",
MaxTokenCap = 2000,
DailyRequestCap = 200,
},
new EventDetails
{
EventId = Guid.NewGuid(),
Title = "Event 3",
Summary = "Lorem ipsum dolor sit amet stet ipsum invidunt amet invidunt magna vero delenit tempor invidunt no rebum eirmod. Duo labore eu no nonumy consequat lobortis consequat consetetur ipsum et ipsum ea eirmod esse. Eirmod rebum voluptua duo et autem eirmod vero amet dolores tincidunt lorem ipsum stet dolore sed aliquyam nonumy consetetur. Rebum no invidunt justo consetetur gubergren sea luptatum ut et amet ut aliquyam lorem ipsum. Nonummy et dolor placerat sit hendrerit invidunt. Et est dolore magna et suscipit duo aliquyam sed dolore ipsum erat nonummy eirmod. Nonummy consequat et et et accusam hendrerit et dolor et. Sanctus gubergren elitr sit takimata accusam lobortis quod sit nonumy nonumy diam clita clita. Ea takimata dolor molestie duo tempor invidunt amet nobis lorem accumsan duo rebum diam ipsum dolores erat ea. Amet nulla eirmod takimata no vel in et sea lobortis ut ullamcorper sadipscing delenit duo takimata ipsum eos consectetuer. Et et ea no duis eu labore quod ipsum feugiat esse lorem clita et nibh iriure diam magna. Sit duis tempor dolore sed et no magna et dolor labore clita erat sed dolores accusam molestie clita. Quis amet eum sit magna kasd eu invidunt nihil. Labore diam erat dignissim labore ipsum qui clita vel eos. Nisl praesent amet consequat ipsum justo quod tempor sed est aliquyam labore lorem accusam diam.",
MaxTokenCap = 3000,
DailyRequestCap = 300,
},
new EventDetails
{
EventId = Guid.NewGuid(),
Title = "Event 4",
Summary = "Summary 4",
MaxTokenCap = 3000,
DailyRequestCap = 300,
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
::deep .event-list {
background-color: var(--neutral-layer-4);
padding-block: 2.0em;
padding-inline: 1.5em;
margin-top: 1rem;
border-radius: 8px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Net;

using AzureOpenAIProxy.AppHost.Tests.Fixtures;

using FluentAssertions;

namespace AzureOpenAIProxy.AppHost.Tests.PlaygroundApp.Pages;

public class EventsPageTests(AspireAppHostFixture host) : IClassFixture<AspireAppHostFixture>
{
[Fact]
public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_OK()
{
// Arrange
using var httpClient = host.App!.CreateHttpClient("playgroundapp");
await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));

// Act
var response = await httpClient.GetAsync("/events");

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}

[Theory]
[InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css")]
public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_CSS_Elements(string expected)
{
// Arrange
using var httpClient = host.App!.CreateHttpClient("playgroundapp");
await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));

// Act
var html = await httpClient.GetStringAsync("/events");

// Assert
html.Should().Contain(expected);
}

[Theory]
[InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/Microsoft.FluentUI.AspNetCore.Components.lib.module.js")]
public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_JavaScript_Elements(string expected)
{
// Arrange
using var httpClient = host.App!.CreateHttpClient("playgroundapp");
await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));

// Act
var html = await httpClient.GetStringAsync("/events");

// Assert
html.Should().Contain(expected);
}

[Theory]
[InlineData("<div class=\"fluent-tooltip-provider\"></div>")]
public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTML_Elements(string expected)
{
// Arrange
using var httpClient = host.App!.CreateHttpClient("playgroundapp");
await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30));

// Act
var html = await httpClient.GetStringAsync("/events");

// Assert
html.Should().Contain(expected);
}
}
93 changes: 93 additions & 0 deletions test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/EventsPageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using FluentAssertions;

using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;

namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages;

[Parallelizable(ParallelScope.Self)]
[TestFixture]
[Property("Category", "Integration")]
public class EventsPageTests : PageTest
{
public override BrowserNewContextOptions ContextOptions() => new()
{
IgnoreHTTPSErrors = true,
};

[SetUp]
public async Task Init()
{
await Page.GotoAsync("https://localhost:5001/events");
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
}

// Grid check
[Test]
public async Task Given_Events_Page_When_Navigated_Then_It_Should_Have_EventListComponent()
{
// Act
var eventListComponent = Page.Locator("div.event-list").First;

// Assert
await Expect(eventListComponent).ToBeVisibleAsync();
}

[Test]
public async Task Given_Events_When_Loaded_Then_It_Should_Have_Less_Than_Or_Equal_To_Four_EventItemComponents()
{
// Arrange
var eventList = Page.Locator("#user-event-list");
var listEvents = await eventList.Locator("div.event-list-item").AllAsync();

// Act
var childrenCount = listEvents.Count;

// Assert
Assert.That(childrenCount, Is.GreaterThan(0));
Assert.That(childrenCount, Is.LessThanOrEqualTo(4));
}

[Test]
public async Task Given_Events_When_Loaded_Then_It_Should_Have_Header_And_Summary_In_The_Card()
{
// Act
var eventCards = await Page.Locator("div.fluent-card-minimal-style.event").AllAsync();

// Assert
foreach (var card in eventCards)
{
card.Should().NotBeNull();
// Check headers
var header = card.Locator("div.fluent-nav-item.event-details-link").First;
await Expect(header).ToBeVisibleAsync();

// Check summaries
var summary = card.Locator("div.event-summary.card.border").First;
await Expect(summary).ToBeVisibleAsync();
}
}

[Test]
public async Task Given_Events_When_Loaded_Then_Their_Links_Are_Enabled_To_Click()
{
// Act
var eventCards = await Page.Locator("div.fluent-card-minimal-style.event").AllAsync();

// Assert
foreach (var card in eventCards)
{
// Getting a link element.
var link = card.Locator("div.fluent-nav-item.event-details-link").First
.Locator("a.fluent-nav-link").First;

await Expect(link).ToBeEnabledAsync();
}
}

[TearDown]
public async Task CleanUp()
{
await Page.CloseAsync();
}
}

0 comments on commit bbdeeb1

Please sign in to comment.