Clear lint backlog so make check runs green
ESLint had 30 findings (13 errors, 17 warnings) that had accumulated
across the codebase. Most came out of --fix; the rest needed small
manual cleanups:
- storage.ts: replace import('zod') type annotations with the already-
imported ZodSchema type
- CityPickerPopup.tsx: drop a stale jsx-a11y disable directive for a
rule that isn't in the shared config, and narrow row.city1 so the
explicit non-null assertions are no longer needed
- keyboardLayoutConverter.ts: guard the per-index reads so we can drop
the trailing ! from the string indexing
- TimeGroup.tsx: narrow actual via the hasDelay condition and default
the day-change numbers to 0 instead of asserting non-null
- seo.ts: throw on the unreachable empty-flightIds branch rather than
fabricating a partial SeoHeadProps
- Various test files: remove captured-but-unused onCity/shouldApply
refs and stale makeStation/emptyCity locals that drifted during
earlier refactors
make check now passes typecheck + lint; the one remaining test
failure is the pre-existing OnlineBoardSearchPage timeout test that
flakes under the full suite and passes in isolation.
This commit is contained in:
@@ -635,13 +635,11 @@ describe("4.1.1-R14/R21: Flight-Map first-entry toggle defaults per TZ §4.1.1",
|
||||
|
||||
it("4.1.1-R14: with geo consent (success) — domestic ON, international ON, transfers OFF", async () => {
|
||||
// Mock geolocation success callback
|
||||
let geoCallback: PositionCallback | null = null;
|
||||
Object.defineProperty(navigator, "geolocation", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: {
|
||||
getCurrentPosition: (cb: PositionCallback) => {
|
||||
geoCallback = cb;
|
||||
// Simulate async position callback on next tick
|
||||
setTimeout(() => {
|
||||
cb({
|
||||
@@ -739,7 +737,7 @@ describe("4.1.1-R14/R21: Flight-Map first-entry toggle defaults per TZ §4.1.1",
|
||||
rerender(<FlightsMapStartPage />);
|
||||
|
||||
// At this point, geo should have fired, setting both domestic and international to true
|
||||
let filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
|
||||
const filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
|
||||
expect(filterValue["domestic"]).toBe(true);
|
||||
expect(filterValue["international"]).toBe(true);
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
|
||||
tileUrl: tileUrlProp,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { locale, language } = useLocale();
|
||||
const { language } = useLocale();
|
||||
|
||||
const {
|
||||
dictionaries,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { FlightsMiniListItem } from "./FlightsMiniListItem.js";
|
||||
import type { ISimpleFlight, IDirectFlight } from "../../types.js";
|
||||
import type { IDirectFlight } from "../../types.js";
|
||||
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => (
|
||||
|
||||
@@ -129,7 +129,7 @@ function LegRoute({
|
||||
arrActualUtc: arrActual?.utc ?? null,
|
||||
});
|
||||
let flightPercent = tlCalc.positionPercent;
|
||||
let elapsedMinutes = tlCalc.elapsedMinutes;
|
||||
const elapsedMinutes = tlCalc.elapsedMinutes;
|
||||
let remainingMinutes = tlCalc.remainingMinutes;
|
||||
if (isFinished) {
|
||||
flightPercent = 100;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { OnlineBoardStartPage, buildOnlineBoardPrefillState } from "./OnlineBoardStartPage.js";
|
||||
import type { PopularRequest } from "@/features/popular-requests/types.js";
|
||||
import {
|
||||
@@ -16,22 +16,16 @@ import {
|
||||
setScheduleFilter,
|
||||
setBoardFilter,
|
||||
type ScheduleFilterSnapshot,
|
||||
type BoardFilterSnapshot,
|
||||
} from "@/shared/state/crossSectionNavigation.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook mocks for geo + viewport — controlled per test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Capture the latest onCity callback so individual tests can invoke it.
|
||||
let capturedOnCity: ((code: string) => void) | null = null;
|
||||
let capturedShouldApply: (() => boolean) | null = null;
|
||||
let geoMockEnabled = false;
|
||||
|
||||
vi.mock("@/shared/hooks/useGeoCityDefault.js", () => ({
|
||||
useGeoCityDefault: (opts: { shouldApply: () => boolean; onCity: (code: string) => void }) => {
|
||||
capturedOnCity = opts.onCity;
|
||||
capturedShouldApply = opts.shouldApply;
|
||||
// If the test enabled geo, fire it synchronously so we don't need async.
|
||||
if (geoMockEnabled && opts.shouldApply()) {
|
||||
opts.onCity("MOW");
|
||||
@@ -335,8 +329,6 @@ describe("4.1.1-R1/R3: first-entry geolocation + defaults", () => {
|
||||
resetCrossSectionStore();
|
||||
geoMockEnabled = false;
|
||||
isMobileMockValue = false;
|
||||
capturedOnCity = null;
|
||||
capturedShouldApply = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
import {
|
||||
getBoardFilter,
|
||||
getScheduleFilter,
|
||||
setBoardFilter,
|
||||
projectScheduleToBoard,
|
||||
} from "@/shared/state/crossSectionNavigation.js";
|
||||
import {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { ScheduleStartPage } from "./ScheduleStartPage.js";
|
||||
import { sessionStore } from "@/shared/storage.js";
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
setBoardFilter,
|
||||
setScheduleFilter,
|
||||
type BoardFilterSnapshot,
|
||||
type ScheduleFilterSnapshot,
|
||||
} from "@/shared/state/crossSectionNavigation.js";
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
@@ -100,15 +99,10 @@ vi.mock("@/shared/dictionaries/index.js", () => ({
|
||||
getCityCodeByAirportCode: () => undefined,
|
||||
}));
|
||||
|
||||
// Capture the latest onCity callback so individual tests can invoke it.
|
||||
let capturedOnCity: ((code: string) => void) | null = null;
|
||||
let capturedShouldApply: (() => boolean) | null = null;
|
||||
let geoMockEnabled = false;
|
||||
|
||||
vi.mock("@/shared/hooks/useGeoCityDefault.js", () => ({
|
||||
useGeoCityDefault: (opts: { shouldApply: () => boolean; onCity: (code: string) => void }) => {
|
||||
capturedOnCity = opts.onCity;
|
||||
capturedShouldApply = opts.shouldApply;
|
||||
// If the test enabled geo, fire it synchronously so we don't need async.
|
||||
if (geoMockEnabled && opts.shouldApply()) {
|
||||
opts.onCity("MOW");
|
||||
@@ -122,8 +116,6 @@ describe("ScheduleStartPage", () => {
|
||||
sessionStore.clear();
|
||||
resetCrossSectionStore();
|
||||
geoMockEnabled = false;
|
||||
capturedOnCity = null;
|
||||
capturedShouldApply = null;
|
||||
// Freeze clock for deterministic week bounds
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0)); // Fri 2026-05-15
|
||||
@@ -367,8 +359,6 @@ describe("4.1.1-R8/R10: Schedule first-entry geolocation + time default", () =>
|
||||
sessionStore.clear();
|
||||
resetCrossSectionStore();
|
||||
geoMockEnabled = false;
|
||||
capturedOnCity = null;
|
||||
capturedShouldApply = null;
|
||||
});
|
||||
|
||||
it("4.1.1-R8: populates departure with geolocated city on first entry", () => {
|
||||
|
||||
@@ -230,7 +230,10 @@ export function buildScheduleDetailsSeo(
|
||||
flightNumbers: flightDisplay,
|
||||
});
|
||||
} else {
|
||||
const first = flightIds[0]!;
|
||||
// Safe: caller passes a non-empty flightIds for the non-connecting
|
||||
// branch (single flight by construction), but TS can't prove it.
|
||||
const first = flightIds[0];
|
||||
if (!first) throw new Error("buildScheduleDetailsSeo: non-connecting branch requires at least one flightId");
|
||||
const flightNumber = `${first.carrier} ${first.flightNumber}${first.suffix ?? ""}`;
|
||||
const routeCities = deriveRouteCities(flights);
|
||||
if (routeCities) {
|
||||
|
||||
@@ -93,7 +93,7 @@ export const sessionStore = {
|
||||
},
|
||||
|
||||
/** Schema-validated get from sessionStorage under the `afl_` namespace. */
|
||||
get<T>(key: string, schema: import("zod").ZodSchema<T>): T | null {
|
||||
get<T>(key: string, schema: ZodSchema<T>): T | null {
|
||||
try {
|
||||
const raw = this.getRaw(SESSION_PREFIX + key);
|
||||
if (raw === null) return null;
|
||||
@@ -106,7 +106,7 @@ export const sessionStore = {
|
||||
},
|
||||
|
||||
/** Schema-validated set into sessionStorage under the `afl_` namespace. */
|
||||
set<T>(key: string, value: T, schema: import("zod").ZodSchema<T>): void {
|
||||
set<T>(key: string, value: T, schema: ZodSchema<T>): void {
|
||||
schema.parse(value);
|
||||
this.setRaw(SESSION_PREFIX + key, JSON.stringify(value));
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent, within } from "@testing-library/react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { CityPickerPopup } from "./CityPickerPopup.js";
|
||||
import { transformDictionaries } from "@/shared/dictionaries/index.js";
|
||||
import type { IRawDictionaries } from "@/shared/dictionaries/index.js";
|
||||
|
||||
@@ -41,9 +41,9 @@ export const CityPickerPopup: FC<CityPickerPopupProps> = ({
|
||||
const flatItems = useMemo<FlatItem[]>(() => {
|
||||
const items: FlatItem[] = [];
|
||||
for (const row of rows) {
|
||||
if (row.city1Airports) {
|
||||
if (row.city1Airports && row.city1) {
|
||||
// multi-airport city
|
||||
items.push({ code: row.city1!.code, id: `picker-item-${row.city1!.code}` });
|
||||
items.push({ code: row.city1.code, id: `picker-item-${row.city1.code}` });
|
||||
for (const ap of row.city1Airports) {
|
||||
items.push({ code: ap.code, id: `picker-item-${ap.code}` });
|
||||
}
|
||||
@@ -103,7 +103,6 @@ export const CityPickerPopup: FC<CityPickerPopupProps> = ({
|
||||
highlightedIndex >= 0 ? (flatItems[highlightedIndex]?.id ?? undefined) : undefined;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className="city-autocomplete-popup"
|
||||
data-testid="city-picker-popup"
|
||||
|
||||
@@ -15,8 +15,9 @@ function buildConverter(keys: string, values: string) {
|
||||
const full: Record<string, string> = {};
|
||||
|
||||
for (let i = keys.length; i--; ) {
|
||||
const k = keys[i]!;
|
||||
const v = values[i]!;
|
||||
const k = keys[i];
|
||||
const v = values[i];
|
||||
if (k === undefined || v === undefined) continue;
|
||||
full[k.toUpperCase()] = v.toUpperCase();
|
||||
full[k] = v;
|
||||
}
|
||||
|
||||
@@ -51,25 +51,6 @@ function makeTimesSet(local: string, dayChangeValue = 0) {
|
||||
};
|
||||
}
|
||||
|
||||
function makeStation(
|
||||
airportCode: string,
|
||||
city: string,
|
||||
airport: string,
|
||||
terminal?: string,
|
||||
) {
|
||||
return {
|
||||
scheduled: {
|
||||
airport,
|
||||
airportCode,
|
||||
city,
|
||||
cityCode: airportCode,
|
||||
countryCode: "RU",
|
||||
},
|
||||
terminal,
|
||||
times: undefined as never, // overridden per dep/arr below
|
||||
};
|
||||
}
|
||||
|
||||
function makeLeg(overrides: {
|
||||
depCode?: string;
|
||||
depCity?: string;
|
||||
@@ -426,11 +407,10 @@ describe("4.1.23 — Уточняется fallback for missing station fields",
|
||||
|
||||
it("T23-fallback: shows SHARED.UNSPECIFIED fallback when departure city is empty string", () => {
|
||||
render(<FlightCard flight={makeFlight({ depCity: "" })} expandable />);
|
||||
// The useCityName mock returns "" for unknown codes when city is empty
|
||||
// StationDisplay should render "SHARED.UNSPECIFIED" for empty city
|
||||
// The useCityName mock returns "" for unknown codes when city is empty.
|
||||
// StationDisplay should emit either SHARED.UNSPECIFIED or real text — at
|
||||
// minimum, at least one station element is rendered.
|
||||
const stationEls = document.querySelectorAll(".station__city--bold");
|
||||
const emptyCity = Array.from(stationEls).find((el) => el.textContent === "" || el.textContent === "SHARED.UNSPECIFIED");
|
||||
// At minimum, no empty-string content is emitted without fallback
|
||||
expect(stationEls.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -81,14 +81,14 @@ export const TimeGroup: FC<TimeGroupProps> = ({
|
||||
<div className="time-group">
|
||||
{label ? <span className="time-group__label">{label}</span> : null}
|
||||
<div className="time-group__times">
|
||||
{hasDelay ? (
|
||||
{hasDelay && actual !== undefined ? (
|
||||
<>
|
||||
<span className="time-group__actual">
|
||||
{actualTime}
|
||||
{actBadge ? (
|
||||
<span
|
||||
className="time-group__day-change"
|
||||
title={dayChangeBadgeTooltip(actual!, actDC!)}
|
||||
title={dayChangeBadgeTooltip(actual, actDC ?? 0)}
|
||||
>
|
||||
{actBadge}
|
||||
</span>
|
||||
@@ -99,7 +99,7 @@ export const TimeGroup: FC<TimeGroupProps> = ({
|
||||
{schedBadge ? (
|
||||
<span
|
||||
className="time-group__day-change time-group__day-change--scheduled"
|
||||
title={dayChangeBadgeTooltip(scheduled, schedDC!)}
|
||||
title={dayChangeBadgeTooltip(scheduled, schedDC ?? 0)}
|
||||
>
|
||||
{schedBadge}
|
||||
</span>
|
||||
@@ -112,7 +112,7 @@ export const TimeGroup: FC<TimeGroupProps> = ({
|
||||
{schedBadge ? (
|
||||
<span
|
||||
className="time-group__day-change"
|
||||
title={dayChangeBadgeTooltip(scheduled, schedDC!)}
|
||||
title={dayChangeBadgeTooltip(scheduled, schedDC ?? 0)}
|
||||
>
|
||||
{schedBadge}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user