Align board search + details with Angular visual parity
Both pages were rendering content directly on the dark-blue page background, which made the flight list and details card effectively invisible. Angular wraps the same content in a white card (section.frame) with drop shadow. Changes: - Wrap FlightList in <section class='frame'> on the search page and wrap the details body the same way. - Replace the inline numbered .calendar-day strip on the search page with the existing <DayTabs> component — the same one the details page already uses (weekday + day + month labels, ‹/› paging). - Pass onFlightClick through FlightList into FlightCard so whole rows are keyboard-accessible buttons, matching Angular's row-level click. The off-screen data-testid='flight-link-*' buttons stay for e2e. - Fix 'Leg NaN' header + the React key warning in FlightLegs when the API returns a Direct leg without an index field. - Update the existing flight-search integration test to target the DayTabs testid instead of the old ad-hoc calendar-strip one. A docs/parity-report-2026-04-17.md file captures the diffs I applied and a punch list of the remaining parity gaps (operator logo on rows, delay/day-change glyphs, Share button visibility on board details, the aircraft panel as a table). Those need per-component work against the Angular templates and will follow in a separate pass.
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
# Visual Parity Report — Angular vs React
|
||||||
|
|
||||||
|
_Captured 2026-04-17, SVO→LED route, SU 6805 details page, viewport 1440×900._
|
||||||
|
|
||||||
|
Screenshots (checked in under `/` root):
|
||||||
|
- `parity-angular-search.png`, `parity-react-search.png`
|
||||||
|
- `parity-angular-details.png`, `parity-react-details.png`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Search results (`/ru/onlineboard/route/SVO-LED-20260417`)
|
||||||
|
|
||||||
|
| Angular | React (pre-fix) | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Flight list sits on a white card with drop shadow (`section.frame`) | Flight rows rendered on the dark-blue page background → text barely readable | **fixed**: wrap `<FlightList>` in `<section class="frame">` |
|
||||||
|
| Calendar strip uses paged weekday tabs: `16 апр.` / `17 апреля` (active, bold) / `18 апр.` … with `‹ ›` arrows | Thin strip of naked numbered buttons wrapping across two rows (`16 17 18 … 1 2 3 …`) | **fixed**: replace inline `.calendar-day` with existing `<DayTabs>` component |
|
||||||
|
| Each row is clickable; hovering highlights the row | Rows are not interactive; navigation to details only via off-screen test-only buttons | **fixed**: `FlightCard` accepts `onClick` and renders with `role="button"` + hover highlight |
|
||||||
|
| Row shows: flight # · operator logo + name · scheduled time (big) · actual time struck-through in orange on delay · city + terminal link · status with plane icon · arrival mirror · expand arrow | Row shows: flight # · scheduled time · city name · status text. Missing operator logo, terminal link, actual-time strike-through, delay arrow glyph, expanded panel | open — operator logo, terminal link, expanded-state layout still pending |
|
||||||
|
| Filter sidebar has collapsible Route accordion + time slider inside; "Sheremetyevo" populated in departure input | Filter sidebar shows `SVO` / `LED` codes and sliders; labels don't render the city name | open — filter should pre-populate with the airport display name |
|
||||||
|
| Footer note `* Время в системе - МЕСТНОЕ.` | Not rendered | open |
|
||||||
|
|
||||||
|
## Details page (`/ru/onlineboard/SU6805-20260417`)
|
||||||
|
|
||||||
|
| Angular | React (pre-fix) | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| All right-hand content on a single white card | Content directly on dark blue — text invisible | **fixed**: wrap in `<section class="frame">` |
|
||||||
|
| `DayTabs` used at the top of the card (already present in React) | Same component, but on dark bg | **fixed** by moving into frame |
|
||||||
|
| React console warning: `Each child in a list should have a unique "key" prop. Check the render method of FlightLegs` | — | **fixed**: key now falls back to the array index when `leg.index` is absent |
|
||||||
|
| Header shows: flight # · airline name · big `РОССИЯ` logo · share button · last-update timestamp | React header currently shows only flight # + share + last-update | open — needs operator name/logo treatment in `BoardDetailsHeader` |
|
||||||
|
| Route row: departure city + plane + status word (`Запланирован`) + arrival city; "+1" day indicator sits above the arrival time | React leg row renders raw station codes, city names, times stacked vertically | open — `FlightLegs` block needs to be rewritten as a horizontal route strip like Angular's `flight-board-details` |
|
||||||
|
| Accordion first section (`Детали рейса`) open by default with Регистрация / Посадка rows visible; others (Борт, Питание, Услуги, Деплайнинг) below | 5 accordions all rendered, all collapsed by default; no `Детали рейса` wrapper | open — open primary section by default, hide `Share` button (Angular hides it on board view) |
|
||||||
|
| `Борт` (aircraft) panel has a table with columns: Название / Количество мест / Эконом / Бизнес / Предыдущий рейс | React's `AircraftPanel` renders the same fields but as a list, not a table | open |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority fixes shipped in this commit
|
||||||
|
|
||||||
|
1. **White card** (`section.frame`) around flight list + details body — the single biggest readability issue.
|
||||||
|
2. **`DayTabs` calendar strip** replaces the inline number-only strip on the search page.
|
||||||
|
3. **Clickable `FlightCard`** with keyboard support and hover highlight, so users don't have to use the off-screen test button.
|
||||||
|
4. **Fixed React key warning** in the `FlightLegs` renderer.
|
||||||
|
|
||||||
|
## Deferred (not shipped)
|
||||||
|
|
||||||
|
- Operator logo + airline name on every row and in the details header.
|
||||||
|
- Delay arrow + `+1` day-change indicators in Angular's layout.
|
||||||
|
- Inline per-row expansion with Registration/Boarding/Deboarding mini-grid.
|
||||||
|
- `Борт` aircraft panel as a table.
|
||||||
|
- Pre-populate the filter with the selected city's display name.
|
||||||
|
- Hide `Share` button on the board details page (Angular only shows it on schedule details).
|
||||||
|
|
||||||
|
These need targeted, per-component changes against the Angular HTML templates referenced in `ClientApp/src/app/modules/pages/board/` and `ClientApp/src/app/modules/pages/details/`. They don't affect readability, so they're listed for a follow-up pass.
|
||||||
@@ -55,10 +55,10 @@ function FlightLegs({
|
|||||||
return (
|
return (
|
||||||
<div className="flight-details__legs" data-testid="flight-legs">
|
<div className="flight-details__legs" data-testid="flight-legs">
|
||||||
{legs.map((leg, i) => (
|
{legs.map((leg, i) => (
|
||||||
<Fragment key={leg.index}>
|
<Fragment key={`leg-${leg.index ?? i}`}>
|
||||||
<div className="flight-details__leg" data-testid={`flight-leg-${leg.index}`}>
|
<div className="flight-details__leg" data-testid={`flight-leg-${leg.index ?? i}`}>
|
||||||
<div className="flight-details__leg-header">
|
<div className="flight-details__leg-header">
|
||||||
<span className="flight-details__leg-index">Leg {leg.index + 1}</span>
|
<span className="flight-details__leg-index">Leg {(leg.index ?? i) + 1}</span>
|
||||||
<span className="flight-details__leg-status">{leg.status}</span>
|
<span className="flight-details__leg-status">{leg.status}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -279,49 +279,51 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flight-details" data-testid="flight-details">
|
<section className="frame">
|
||||||
{/* Connection status */}
|
<div className="flight-details" data-testid="flight-details">
|
||||||
<div className="flight-details__status" data-testid="connection-status">
|
{/* Connection status */}
|
||||||
{connectionStatus === "live" && (
|
<div className="flight-details__status" data-testid="connection-status">
|
||||||
<span className="connection-badge connection-badge--live">Live</span>
|
{connectionStatus === "live" && (
|
||||||
)}
|
<span className="connection-badge connection-badge--live">Live</span>
|
||||||
{connectionStatus === "reconnecting" && (
|
)}
|
||||||
<span className="connection-badge connection-badge--reconnecting">Reconnecting...</span>
|
{connectionStatus === "reconnecting" && (
|
||||||
)}
|
<span className="connection-badge connection-badge--reconnecting">Reconnecting...</span>
|
||||||
{connectionStatus === "offline" && (
|
)}
|
||||||
<span className="connection-badge connection-badge--offline">Offline</span>
|
{connectionStatus === "offline" && (
|
||||||
)}
|
<span className="connection-badge connection-badge--offline">Offline</span>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<BoardDetailsHeader flight={displayFlight} locale={locale} />
|
|
||||||
|
|
||||||
{displayFlight.routeType === "MultiLeg" && (
|
|
||||||
<FullRouteTimeline legs={displayFlight.legs} viewType="Onlineboard" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Summary card */}
|
|
||||||
<FlightCard flight={displayFlight} />
|
|
||||||
|
|
||||||
{/* Operating carrier */}
|
|
||||||
{displayFlight.operatingBy.carrier && (
|
|
||||||
<div className="flight-details__operating" data-testid="operating-carrier">
|
|
||||||
Operated by: {displayFlight.operatingBy.carrier}
|
|
||||||
{displayFlight.operatingBy.flightNumber
|
|
||||||
? ` ${displayFlight.operatingBy.flightNumber}`
|
|
||||||
: ""}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Detailed leg information */}
|
<BoardDetailsHeader flight={displayFlight} locale={locale} />
|
||||||
<FlightLegs legs={legs} viewType="Onlineboard" />
|
|
||||||
|
|
||||||
{/* Flying time */}
|
{displayFlight.routeType === "MultiLeg" && (
|
||||||
<div className="flight-details__flying-time" data-testid="flying-time">
|
<FullRouteTimeline legs={displayFlight.legs} viewType="Onlineboard" />
|
||||||
Total flying time: {displayFlight.flyingTime}
|
)}
|
||||||
|
|
||||||
|
{/* Summary card */}
|
||||||
|
<FlightCard flight={displayFlight} />
|
||||||
|
|
||||||
|
{/* Operating carrier */}
|
||||||
|
{displayFlight.operatingBy.carrier && (
|
||||||
|
<div className="flight-details__operating" data-testid="operating-carrier">
|
||||||
|
Operated by: {displayFlight.operatingBy.carrier}
|
||||||
|
{displayFlight.operatingBy.flightNumber
|
||||||
|
? ` ${displayFlight.operatingBy.flightNumber}`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Detailed leg information */}
|
||||||
|
<FlightLegs legs={legs} viewType="Onlineboard" />
|
||||||
|
|
||||||
|
{/* Flying time */}
|
||||||
|
<div className="flight-details__flying-time" data-testid="flying-time">
|
||||||
|
Total flying time: {displayFlight.flyingTime}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FlightSchedule flight={displayFlight} />
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<FlightSchedule flight={displayFlight} />
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { PageLayout } from "@/ui/layout/PageLayout.js";
|
|||||||
import { PageTabs } from "@/ui/layout/PageTabs.js";
|
import { PageTabs } from "@/ui/layout/PageTabs.js";
|
||||||
import { SearchHistory } from "@/ui/layout/SearchHistory.js";
|
import { SearchHistory } from "@/ui/layout/SearchHistory.js";
|
||||||
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";
|
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";
|
||||||
|
import { DayTabs } from "./DayTabs/index.js";
|
||||||
import "./OnlineBoardSearchPage.scss";
|
import "./OnlineBoardSearchPage.scss";
|
||||||
import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
|
import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
|
||||||
import { useOnlineBoard } from "../hooks/useOnlineBoard.js";
|
import { useOnlineBoard } from "../hooks/useOnlineBoard.js";
|
||||||
@@ -37,16 +38,6 @@ export interface OnlineBoardSearchPageProps {
|
|||||||
params: OnlineBoardParams & { type: "flight" | "departure" | "arrival" | "route" };
|
params: OnlineBoardParams & { type: "flight" | "departure" | "arrival" | "route" };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format a yyyyMMdd date string for display in the calendar strip.
|
|
||||||
* Shows the day number (e.g. "15", "16").
|
|
||||||
*/
|
|
||||||
function formatDayLabel(yyyymmdd: string): string {
|
|
||||||
if (yyyymmdd.length !== 8) return yyyymmdd;
|
|
||||||
const day = parseInt(yyyymmdd.slice(6, 8), 10);
|
|
||||||
return String(day);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert yyyyMMdd URL date to API format (yyyy-MM-ddT00:00:00).
|
* Convert yyyyMMdd URL date to API format (yyyy-MM-ddT00:00:00).
|
||||||
*/
|
*/
|
||||||
@@ -296,20 +287,14 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
stickyContent={
|
stickyContent={
|
||||||
calendarDays.length > 0 ? (
|
<DayTabs
|
||||||
<div className="online-board-search__calendar" data-testid="calendar-strip">
|
selectedDate={params.date}
|
||||||
{calendarDays.map((day) => (
|
availableDates={calendarDays}
|
||||||
<button
|
daysBefore={2}
|
||||||
key={day}
|
daysAfter={14}
|
||||||
type="button"
|
locale={lang}
|
||||||
className={`calendar-day${day === params.date ? " calendar-day--active" : ""}`}
|
onNavigate={handleDateChange}
|
||||||
onClick={() => handleDateChange(day)}
|
/>
|
||||||
>
|
|
||||||
{formatDayLabel(day)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Connection status indicator */}
|
{/* Connection status indicator */}
|
||||||
@@ -346,10 +331,19 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Flight list */}
|
{/* Flight list — wrapped in .frame for the white card + shadow */}
|
||||||
{!error && <FlightList flights={displayFlights} loading={loading} />}
|
{!error && (
|
||||||
|
<section className="frame">
|
||||||
|
<FlightList
|
||||||
|
flights={displayFlights}
|
||||||
|
loading={loading}
|
||||||
|
onFlightClick={handleFlightClick}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Flight click overlay — we make the list clickable */}
|
{/* Off-screen hit targets for e2e tests — shares markup contract
|
||||||
|
with previous versions of the page. */}
|
||||||
{!loading && displayFlights.length > 0 && (
|
{!loading && displayFlights.length > 0 && (
|
||||||
<div className="online-board-search__actions" data-testid="flight-actions">
|
<div className="online-board-search__actions" data-testid="flight-actions">
|
||||||
{displayFlights.map((flight) => (
|
{displayFlights.map((flight) => (
|
||||||
|
|||||||
@@ -9,11 +9,26 @@
|
|||||||
padding: vars.$space-xl 0;
|
padding: vars.$space-xl 0;
|
||||||
margin: 0 vars.$space-xl;
|
margin: 0 vars.$space-xl;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
background: transparent;
|
||||||
|
transition: background-color 120ms ease;
|
||||||
|
|
||||||
& + & {
|
& + & {
|
||||||
border-top: 1px dashed colors.$border;
|
border-top: 1px dashed colors.$border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eef3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid colors.$blue;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__number {
|
&__number {
|
||||||
width: vars.$width-flight-number;
|
width: vars.$width-flight-number;
|
||||||
font-weight: fonts.$font-medium;
|
font-weight: fonts.$font-medium;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FC } from "react";
|
import type { FC, KeyboardEvent } from "react";
|
||||||
import type { ISimpleFlight, IFlightLeg } from "@/features/online-board/types.js";
|
import type { ISimpleFlight, IFlightLeg } from "@/features/online-board/types.js";
|
||||||
import { StationDisplay } from "./StationDisplay.js";
|
import { StationDisplay } from "./StationDisplay.js";
|
||||||
import { TimeGroup } from "./TimeGroup.js";
|
import { TimeGroup } from "./TimeGroup.js";
|
||||||
@@ -8,6 +8,7 @@ import "./FlightCard.scss";
|
|||||||
|
|
||||||
export interface FlightCardProps {
|
export interface FlightCardProps {
|
||||||
flight: ISimpleFlight;
|
flight: ISimpleFlight;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Extract the primary leg from a flight (first leg for multi-leg) */
|
/** Extract the primary leg from a flight (first leg for multi-leg) */
|
||||||
@@ -40,7 +41,7 @@ function flyingTimeToMinutes(flyingTime: string): number {
|
|||||||
*
|
*
|
||||||
* Composes StationDisplay + TimeGroup + FlightStatus + DurationDisplay.
|
* Composes StationDisplay + TimeGroup + FlightStatus + DurationDisplay.
|
||||||
*/
|
*/
|
||||||
export const FlightCard: FC<FlightCardProps> = ({ flight }) => {
|
export const FlightCard: FC<FlightCardProps> = ({ flight, onClick }) => {
|
||||||
const departureLeg = getPrimaryLeg(flight);
|
const departureLeg = getPrimaryLeg(flight);
|
||||||
const arrivalLeg = getFinalLeg(flight);
|
const arrivalLeg = getFinalLeg(flight);
|
||||||
|
|
||||||
@@ -50,9 +51,26 @@ export const FlightCard: FC<FlightCardProps> = ({ flight }) => {
|
|||||||
const arrTimes = arrStation.times;
|
const arrTimes = arrStation.times;
|
||||||
|
|
||||||
const flightNumber = `${flight.flightId.carrier} ${flight.flightId.flightNumber}`;
|
const flightNumber = `${flight.flightId.carrier} ${flight.flightId.flightNumber}`;
|
||||||
|
const clickable = Boolean(onClick);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flight-card" data-flight-id={flight.id}>
|
<div
|
||||||
|
className={`flight-card${clickable ? " flight-card--clickable" : ""}`}
|
||||||
|
data-flight-id={flight.id}
|
||||||
|
{...(clickable
|
||||||
|
? {
|
||||||
|
role: "button",
|
||||||
|
tabIndex: 0,
|
||||||
|
onClick,
|
||||||
|
onKeyDown: (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick!();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
|
>
|
||||||
<div className="flight-card__number">{flightNumber}</div>
|
<div className="flight-card__number">{flightNumber}</div>
|
||||||
|
|
||||||
<div className="flight-card__route">
|
<div className="flight-card__route">
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface FlightListProps {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
/** Number of skeleton rows when loading (default: 5) */
|
/** Number of skeleton rows when loading (default: 5) */
|
||||||
skeletonCount?: number;
|
skeletonCount?: number;
|
||||||
|
/** Click handler — makes each card act as a link to the details page */
|
||||||
|
onFlightClick?: (flight: ISimpleFlight) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +25,7 @@ export const FlightList: FC<FlightListProps> = ({
|
|||||||
flights,
|
flights,
|
||||||
loading = false,
|
loading = false,
|
||||||
skeletonCount = 5,
|
skeletonCount = 5,
|
||||||
|
onFlightClick,
|
||||||
}) => {
|
}) => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <FlightListSkeleton count={skeletonCount} />;
|
return <FlightListSkeleton count={skeletonCount} />;
|
||||||
@@ -39,7 +42,11 @@ export const FlightList: FC<FlightListProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flight-list">
|
<div className="flight-list">
|
||||||
{flights.map((flight) => (
|
{flights.map((flight) => (
|
||||||
<FlightCard key={flight.id} flight={flight} />
|
<FlightCard
|
||||||
|
key={flight.id}
|
||||||
|
flight={flight}
|
||||||
|
{...(onFlightClick ? { onClick: () => onFlightClick(flight) } : {})}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -156,15 +156,15 @@ describe("Flight search page integration", () => {
|
|||||||
expect(screen.getByText("No flights found")).toBeTruthy();
|
expect(screen.getByText("No flights found")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders calendar strip with available days", () => {
|
it("renders calendar strip (DayTabs) with pagination arrows", () => {
|
||||||
setupMocksWithData();
|
setupMocksWithData();
|
||||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||||
const calendarStrip = screen.getByTestId("calendar-strip");
|
// Pre-refactor the sticky strip was an ad-hoc DOM with data-testid
|
||||||
expect(calendarStrip).toBeTruthy();
|
// "calendar-strip". We now use the shared <DayTabs> component, whose
|
||||||
// Verify calendar day buttons are present
|
// root testid is "day-tabs". Assert its structural markers instead.
|
||||||
for (const day of CALENDAR_DAYS) {
|
expect(screen.getByTestId("day-tabs")).toBeTruthy();
|
||||||
expect(screen.getByText(day)).toBeTruthy();
|
expect(screen.getByTestId("day-tabs-prev")).toBeTruthy();
|
||||||
}
|
expect(screen.getByTestId("day-tabs-next")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders live connection badge when SignalR is live", () => {
|
it("renders live connection badge when SignalR is live", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user