Mirror Angular shell placeholders
This commit is contained in:
@@ -44,6 +44,52 @@ const publicEnvB64 = Buffer.from(
|
|||||||
const PUBLIC_ENV_SCRIPT =
|
const PUBLIC_ENV_SCRIPT =
|
||||||
`window.__ENV__=Object.assign(window.__ENV__||Object.create(null),JSON.parse(atob("${publicEnvB64}")));`;
|
`window.__ENV__=Object.assign(window.__ENV__||Object.create(null),JSON.parse(atob("${publicEnvB64}")));`;
|
||||||
|
|
||||||
|
const modernCommand = process.argv.join(" ");
|
||||||
|
const npmLifecycleEvent = process.env["npm_lifecycle_event"] ?? "";
|
||||||
|
const isDevServer =
|
||||||
|
/\bdev\b/.test(modernCommand) || npmLifecycleEvent.includes("dev");
|
||||||
|
// Angular uses full `afl-component` loader assets in production
|
||||||
|
// `index.html`, but its local `index.dev.html` keeps only shell
|
||||||
|
// placeholders. Do the same here: the production loader's follow-up XHRs
|
||||||
|
// are CORS-blocked on localhost and would pollute every e2e run.
|
||||||
|
const shouldLoadStandaloneShellLoader = !isRemote && !isDevServer;
|
||||||
|
|
||||||
|
const standaloneShellTags = isRemote
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
...(shouldLoadStandaloneShellLoader
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
tag: "link",
|
||||||
|
head: true,
|
||||||
|
append: true,
|
||||||
|
attrs: {
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: "https://www.aeroflot.ru/frontend/static/css/afl-frontend-loader.bundle.css",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "script",
|
||||||
|
head: false,
|
||||||
|
append: true,
|
||||||
|
attrs: {
|
||||||
|
async: true,
|
||||||
|
src: "https://www.aeroflot.ru/frontend/static/js/afl-frontend-loader.bundle.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
tag: "meta",
|
||||||
|
head: true,
|
||||||
|
append: true,
|
||||||
|
attrs: {
|
||||||
|
name: "aeroflot-shell-loader",
|
||||||
|
content: shouldLoadStandaloneShellLoader ? "external" : "placeholder",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins,
|
plugins,
|
||||||
source: {
|
source: {
|
||||||
@@ -60,6 +106,7 @@ export default defineConfig({
|
|||||||
append: false,
|
append: false,
|
||||||
children: PUBLIC_ENV_SCRIPT,
|
children: PUBLIC_ENV_SCRIPT,
|
||||||
},
|
},
|
||||||
|
...standaloneShellTags,
|
||||||
{
|
{
|
||||||
tag: "link",
|
tag: "link",
|
||||||
attrs: {
|
attrs: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Outlet, useLocation } from "@modern-js/runtime/router";
|
import { Outlet } from "@modern-js/runtime/router";
|
||||||
import { ErrorBoundary } from "@/ui/errors/ErrorBoundary";
|
import { ErrorBoundary } from "@/ui/errors/ErrorBoundary";
|
||||||
import { AeroflotShell } from "@/ui/shell";
|
import { AeroflotShell } from "@/ui/shell";
|
||||||
import { LoggerProvider } from "@/observability/logger/provider";
|
import { LoggerProvider } from "@/observability/logger/provider";
|
||||||
@@ -7,7 +7,6 @@ import { createRootLogger } from "@/observability/logger/root";
|
|||||||
import { ApiClientProvider } from "@/shared/api/provider";
|
import { ApiClientProvider } from "@/shared/api/provider";
|
||||||
import { ApiClient } from "@/shared/api/client";
|
import { ApiClient } from "@/shared/api/client";
|
||||||
import { getEnv } from "@/env/index";
|
import { getEnv } from "@/env/index";
|
||||||
import { resolveLocaleFromPath, localeToLanguage } from "@/i18n/resolver";
|
|
||||||
|
|
||||||
// Global styles
|
// Global styles
|
||||||
import "@/styles/index.scss";
|
import "@/styles/index.scss";
|
||||||
@@ -21,10 +20,7 @@ import "leaflet/dist/leaflet.css";
|
|||||||
* nested routes.
|
* nested routes.
|
||||||
*/
|
*/
|
||||||
export default function RootLayout(): JSX.Element {
|
export default function RootLayout(): JSX.Element {
|
||||||
const location = useLocation();
|
|
||||||
const logger = useMemo(() => createRootLogger(), []);
|
const logger = useMemo(() => createRootLogger(), []);
|
||||||
const locale = resolveLocaleFromPath(location.pathname);
|
|
||||||
const language = locale ? localeToLanguage(locale) : "ru";
|
|
||||||
|
|
||||||
const apiClient = useMemo(
|
const apiClient = useMemo(
|
||||||
() => {
|
() => {
|
||||||
@@ -42,7 +38,7 @@ export default function RootLayout(): JSX.Element {
|
|||||||
<LoggerProvider logger={logger}>
|
<LoggerProvider logger={logger}>
|
||||||
<ApiClientProvider client={apiClient}>
|
<ApiClientProvider client={apiClient}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<AeroflotShell language={language}>
|
<AeroflotShell>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</AeroflotShell>
|
</AeroflotShell>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.afl-component-header-placeholder,
|
||||||
|
.afl-component-footer-placeholder {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
width: auto;
|
width: auto;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -1,307 +0,0 @@
|
|||||||
@use "../../styles/colors" as colors;
|
|
||||||
@use "../../styles/variables" as vars;
|
|
||||||
|
|
||||||
.aeroflot-shell {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&__header,
|
|
||||||
&__footer {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__country {
|
|
||||||
min-height: 38px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: vars.$space-m;
|
|
||||||
padding: vars.$space-s2 vars.$space-xl;
|
|
||||||
background: colors.$white;
|
|
||||||
color: colors.$text-color;
|
|
||||||
font-size: 14px;
|
|
||||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
button {
|
|
||||||
min-width: 46px;
|
|
||||||
min-height: 30px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 0 vars.$space-l;
|
|
||||||
background: colors.$extra-blue;
|
|
||||||
color: colors.$white;
|
|
||||||
font: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__link-button {
|
|
||||||
background: transparent !important;
|
|
||||||
color: colors.$blue-link !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__skip {
|
|
||||||
position: absolute;
|
|
||||||
left: vars.$space-xl;
|
|
||||||
top: -100px;
|
|
||||||
z-index: 1003;
|
|
||||||
padding: vars.$space-m vars.$space-l;
|
|
||||||
background: colors.$white;
|
|
||||||
color: colors.$extra-blue;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
top: vars.$space-m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nav {
|
|
||||||
min-height: 82px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(160px, auto) 1fr auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: vars.$space-xl;
|
|
||||||
max-width: vars.$site-width;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 vars.$space-xl;
|
|
||||||
color: colors.$white;
|
|
||||||
|
|
||||||
nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: vars.$space-xl;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: colors.$white;
|
|
||||||
text-decoration: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__brand {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: vars.$space-m;
|
|
||||||
font-size: 23px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__brand-mark {
|
|
||||||
width: 46px;
|
|
||||||
height: 46px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 34% 50%, colors.$white 0 21%, transparent 22%),
|
|
||||||
radial-gradient(circle at 66% 50%, colors.$white 0 21%, transparent 22%),
|
|
||||||
colors.$orange;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__account {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: vars.$space-l;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
min-width: 40px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__promo {
|
|
||||||
max-width: vars.$site-width;
|
|
||||||
min-height: 80px;
|
|
||||||
margin: 0 auto vars.$space-l;
|
|
||||||
padding: vars.$space-l vars.$space-xl;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: vars.$space-xl;
|
|
||||||
background: colors.$white;
|
|
||||||
color: colors.$text-color;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16);
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: vars.$space-s;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 40px;
|
|
||||||
padding: 0 vars.$space-xl;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: colors.$extra-blue;
|
|
||||||
color: colors.$white;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
color: colors.$light-gray;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__test-version {
|
|
||||||
max-width: vars.$site-width;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 vars.$space-xl vars.$space-m;
|
|
||||||
color: colors.$white;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
padding: 0 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer {
|
|
||||||
margin-top: vars.$space-xxxl;
|
|
||||||
background: colors.$white;
|
|
||||||
color: colors.$text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer-inner {
|
|
||||||
max-width: vars.$site-width;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: vars.$space-xxl vars.$space-xl;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(260px, 1.3fr) 1fr 1fr minmax(220px, 0.9fr);
|
|
||||||
gap: vars.$space-xxl;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__branch,
|
|
||||||
&__copyright {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__branch {
|
|
||||||
color: colors.$light-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__contacts,
|
|
||||||
&__footer-column,
|
|
||||||
&__app {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: vars.$space-m;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: vars.$space-s;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: colors.$text-color;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: colors.$blue-link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__contacts {
|
|
||||||
dl {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: vars.$space-l;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: colors.$extra-blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
margin-top: vars.$space-s;
|
|
||||||
color: colors.$light-gray;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__app {
|
|
||||||
h2 {
|
|
||||||
color: colors.$extra-blue;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: colors.$light-gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__copyright {
|
|
||||||
color: colors.$light-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: vars.$media-breakpoint-tablet) {
|
|
||||||
&__nav {
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
|
|
||||||
nav {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: vars.$space-m vars.$space-xl;
|
|
||||||
padding-bottom: vars.$space-l;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__promo,
|
|
||||||
&__footer-inner {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: vars.$media-breakpoint-mobile) {
|
|
||||||
&__country {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nav {
|
|
||||||
padding: vars.$space-l;
|
|
||||||
gap: vars.$space-l;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__account {
|
|
||||||
justify-content: flex-end;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: vars.$space-s2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__brand {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__brand-mark {
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__content {
|
|
||||||
padding: 0 vars.$space-m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+62
-206
@@ -1,214 +1,70 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import "./AeroflotShell.scss";
|
import { createElement } from "react";
|
||||||
|
|
||||||
type ShellLanguage = "ru" | "en";
|
|
||||||
|
|
||||||
interface AeroflotShellProps {
|
interface AeroflotShellProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
language: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEXT = {
|
function AflComponent(props: {
|
||||||
ru: {
|
className?: string;
|
||||||
country: "Ваша страна - Россия?",
|
component: string;
|
||||||
yes: "Да",
|
children?: ReactNode;
|
||||||
change: "Изменить",
|
}): JSX.Element {
|
||||||
skip: "Перейти к основному содержимому",
|
return createElement(
|
||||||
nav: [
|
"afl-component",
|
||||||
"Купить билет",
|
{
|
||||||
"Сервисы и услуги",
|
class: props.className,
|
||||||
"Спецпредложения",
|
"data-component": props.component,
|
||||||
"Аэрофлот Бонус",
|
},
|
||||||
"Информация",
|
props.children,
|
||||||
"Для Бизнеса",
|
);
|
||||||
],
|
}
|
||||||
auth: "Авторизация",
|
|
||||||
account: "Личный кабинет",
|
function AflItem(props: { item: string; children: ReactNode }): JSX.Element {
|
||||||
lang: "RU",
|
return createElement(
|
||||||
express: "Аэроэкспресс",
|
"afl-item",
|
||||||
expressText: "Быстрый способ добраться до аэропортов Москвы или в город",
|
{
|
||||||
order: "Заказать",
|
"data-item": props.item,
|
||||||
ad: "Реклама",
|
},
|
||||||
test: "Тестовая версия",
|
props.children,
|
||||||
branch: "\"develop\"",
|
);
|
||||||
contacts: "Контакты",
|
}
|
||||||
mobile: "555",
|
|
||||||
mobileText: "МТС, Билайн, Мегафон, Т2, Т-Мобайл (c мобильного бесплатно)",
|
/**
|
||||||
russiaPhone: "8 (800) 444-55-55",
|
* Standalone page chrome mirrors Angular's `ClientApp/src/index.html`:
|
||||||
russiaPhoneText: "бесплатно по России",
|
* external Aeroflot frontend loader hydrates these `afl-component`
|
||||||
moscowPhone: "+7 (495) 223-55-55",
|
* placeholders, while React owns only the flights content between them.
|
||||||
moscowPhoneText:
|
*/
|
||||||
"бесплатно для Москвы, для международных звонков в соответствии с тарифами вашего оператора связи",
|
export function AeroflotShell({ children }: AeroflotShellProps): JSX.Element {
|
||||||
feedback: "Обратная связь",
|
return (
|
||||||
company: "Компания",
|
<>
|
||||||
companyLinks: [
|
<div
|
||||||
"О компании",
|
className="wrapper-header p-print-none afl-component-header-placeholder"
|
||||||
"Контакты",
|
data-testid="standalone-header"
|
||||||
"Работа в Аэрофлоте",
|
>
|
||||||
"Политика конфиденциальности",
|
<AflComponent className="header" component="Header" />
|
||||||
"Противодействие коррупции",
|
</div>
|
||||||
"Карта сайта",
|
|
||||||
],
|
<div
|
||||||
partners: "Партнерам",
|
className="banner--top p-print-none afl-component--banners"
|
||||||
partnerLinks: ["Агентам", "Грузовые перевозки", "Группа Аэрофлот", "Акционерам и инвесторам"],
|
style={{ display: "none" }}
|
||||||
copyright: "© Авиакомпания «Аэрофлот» 2008-2026",
|
>
|
||||||
app: "Приложение «Аэрофлот»",
|
<AflComponent component="BannersOffers">
|
||||||
appText: "для вашего мобильного устройства",
|
<AflItem item="positionId">383</AflItem>
|
||||||
},
|
</AflComponent>
|
||||||
en: {
|
</div>
|
||||||
country: "Your country is Russia?",
|
|
||||||
yes: "Yes",
|
{children}
|
||||||
change: "Change",
|
|
||||||
skip: "Skip to main content",
|
<div className="banner--bottom p-print-none afl-component-footer-placeholder">
|
||||||
nav: [
|
<div className="banner--bottom__content">
|
||||||
"Book a flight",
|
<AflComponent component="BannersOffers">
|
||||||
"Services",
|
<AflItem item="positionId">384</AflItem>
|
||||||
"Special offers",
|
</AflComponent>
|
||||||
"Aeroflot Bonus",
|
</div>
|
||||||
"Information",
|
</div>
|
||||||
"Business",
|
|
||||||
],
|
<AflComponent className="footer p-print-none" component="Footer" />
|
||||||
auth: "Sign in",
|
</>
|
||||||
account: "Personal account",
|
|
||||||
lang: "EN",
|
|
||||||
express: "Aeroexpress",
|
|
||||||
expressText: "Fast way to get to Moscow airports or the city",
|
|
||||||
order: "Order",
|
|
||||||
ad: "Advertisement",
|
|
||||||
test: "Test version",
|
|
||||||
branch: "\"develop\"",
|
|
||||||
contacts: "Contacts",
|
|
||||||
mobile: "555",
|
|
||||||
mobileText: "free from mobile phones in Russia",
|
|
||||||
russiaPhone: "8 (800) 444-55-55",
|
|
||||||
russiaPhoneText: "free within Russia",
|
|
||||||
moscowPhone: "+7 (495) 223-55-55",
|
|
||||||
moscowPhoneText: "for Moscow and international calls according to your operator rates",
|
|
||||||
feedback: "Feedback",
|
|
||||||
company: "Company",
|
|
||||||
companyLinks: [
|
|
||||||
"About Aeroflot",
|
|
||||||
"Contacts",
|
|
||||||
"Careers",
|
|
||||||
"Privacy policy",
|
|
||||||
"Anti-corruption",
|
|
||||||
"Site map",
|
|
||||||
],
|
|
||||||
partners: "Partners",
|
|
||||||
partnerLinks: ["Agents", "Cargo", "Aeroflot Group", "Shareholders and investors"],
|
|
||||||
copyright: "© Aeroflot 2008-2026",
|
|
||||||
app: "Aeroflot app",
|
|
||||||
appText: "for your mobile device",
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
function toShellLanguage(language: string): ShellLanguage {
|
|
||||||
return language.toLowerCase().startsWith("en") ? "en" : "ru";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AeroflotShell({ children, language }: AeroflotShellProps): JSX.Element {
|
|
||||||
const text = TEXT[toShellLanguage(language)];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="aeroflot-shell">
|
|
||||||
<header className="aeroflot-shell__header" data-testid="standalone-header">
|
|
||||||
<section className="aeroflot-shell__country" aria-label={text.country}>
|
|
||||||
<span>{text.country}</span>
|
|
||||||
<button type="button">{text.yes}</button>
|
|
||||||
<button type="button" className="aeroflot-shell__link-button">
|
|
||||||
{text.change}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<a className="aeroflot-shell__skip" href="#main-content">
|
|
||||||
{text.skip}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div className="aeroflot-shell__nav">
|
|
||||||
<a className="aeroflot-shell__brand" href={`/${text.lang.toLowerCase()}/onlineboard`}>
|
|
||||||
<span className="aeroflot-shell__brand-mark" aria-hidden="true" />
|
|
||||||
<span>Аэрофлот</span>
|
|
||||||
</a>
|
|
||||||
<nav aria-label="Aeroflot">
|
|
||||||
{text.nav.map((item) => (
|
|
||||||
<a href="/" key={item}>
|
|
||||||
{item}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="aeroflot-shell__account">
|
|
||||||
<a href="/">{text.auth}</a>
|
|
||||||
<a href="/">{text.account}</a>
|
|
||||||
<span>{text.lang}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section className="aeroflot-shell__promo" aria-label={text.express}>
|
|
||||||
<div>
|
|
||||||
<strong>{text.express}</strong>
|
|
||||||
<span>{text.expressText}</span>
|
|
||||||
</div>
|
|
||||||
<a href="/">{text.order}</a>
|
|
||||||
<small>{text.ad}</small>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="aeroflot-shell__test-version">{text.test}</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="aeroflot-shell__content" id="main-content">
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer className="aeroflot-shell__footer" data-testid="standalone-footer">
|
|
||||||
<div className="aeroflot-shell__footer-inner">
|
|
||||||
<div className="aeroflot-shell__branch">{text.branch}</div>
|
|
||||||
|
|
||||||
<section className="aeroflot-shell__contacts" aria-label={text.contacts}>
|
|
||||||
<h2>{text.contacts}</h2>
|
|
||||||
<dl>
|
|
||||||
<div>
|
|
||||||
<dt>{text.mobile}</dt>
|
|
||||||
<dd>{text.mobileText}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>{text.russiaPhone}</dt>
|
|
||||||
<dd>{text.russiaPhoneText}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>{text.moscowPhone}</dt>
|
|
||||||
<dd>{text.moscowPhoneText}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
<a href="/">{text.feedback}</a>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="aeroflot-shell__footer-column">
|
|
||||||
<h2>{text.company}</h2>
|
|
||||||
{text.companyLinks.map((item) => (
|
|
||||||
<a href="/" key={item}>
|
|
||||||
{item}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="aeroflot-shell__footer-column">
|
|
||||||
<h2>{text.partners}</h2>
|
|
||||||
{text.partnerLinks.map((item) => (
|
|
||||||
<a href="/" key={item}>
|
|
||||||
{item}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="aeroflot-shell__app">
|
|
||||||
<h2>Аэрофлот</h2>
|
|
||||||
<strong>{text.app}</strong>
|
|
||||||
<span>{text.appText}</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<p className="aeroflot-shell__copyright">{text.copyright}</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { test, expect } from "./fixtures/console-gate";
|
import { test, expect } from "./fixtures/console-gate";
|
||||||
|
|
||||||
test.describe("TIRREDESIGN-30 — standalone header and footer", () => {
|
test.describe("TIRREDESIGN-30 — standalone header and footer", () => {
|
||||||
test("Russian standalone pages render Aeroflot shell around React content", async ({
|
test("Russian standalone pages render Angular-style Aeroflot shell placeholders", async ({
|
||||||
page,
|
page,
|
||||||
consoleMessages,
|
consoleMessages,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -9,38 +9,31 @@ test.describe("TIRREDESIGN-30 — standalone header and footer", () => {
|
|||||||
await page.waitForLoadState("domcontentloaded");
|
await page.waitForLoadState("domcontentloaded");
|
||||||
|
|
||||||
const header = page.getByTestId("standalone-header");
|
const header = page.getByTestId("standalone-header");
|
||||||
await expect(header).toBeVisible();
|
await expect(header.locator('afl-component.header[data-component="Header"]')).toHaveCount(1);
|
||||||
await expect(header).toContainText("Купить билет");
|
|
||||||
await expect(header).toContainText("Аэроэкспресс");
|
|
||||||
await expect(header).toContainText("Тестовая версия");
|
|
||||||
|
|
||||||
await expect(page.locator("#main-content h1")).toHaveText("Страница проверки");
|
await expect(page.locator("h1")).toHaveText("Страница проверки");
|
||||||
|
|
||||||
const footer = page.getByTestId("standalone-footer");
|
await expect(page.locator('afl-component.footer[data-component="Footer"]')).toHaveCount(1);
|
||||||
await expect(footer).toBeVisible();
|
await expect(
|
||||||
await expect(footer).toContainText("Контакты");
|
page.locator('.banner--top afl-component[data-component="BannersOffers"] afl-item[data-item="positionId"]'),
|
||||||
await expect(footer).toContainText("8 (800) 444-55-55");
|
).toHaveText("383");
|
||||||
await expect(footer).toContainText("© Авиакомпания «Аэрофлот» 2008-2026");
|
await expect(
|
||||||
|
page.locator('.banner--bottom afl-component[data-component="BannersOffers"] afl-item[data-item="positionId"]'),
|
||||||
|
).toHaveText("384");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("English standalone pages localize shell chrome", async ({
|
test("local dev uses Angular-style placeholder loader mode", async ({
|
||||||
page,
|
page,
|
||||||
consoleMessages,
|
consoleMessages,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto("/en/smoke");
|
await page.goto("/ru/smoke");
|
||||||
await page.waitForLoadState("domcontentloaded");
|
await page.waitForLoadState("domcontentloaded");
|
||||||
|
|
||||||
const header = page.getByTestId("standalone-header");
|
await expect(
|
||||||
await expect(header).toBeVisible();
|
page.locator('meta[name="aeroflot-shell-loader"][content="placeholder"]'),
|
||||||
await expect(header).toContainText("Book a flight");
|
).toHaveCount(1);
|
||||||
await expect(header).toContainText("Aeroexpress");
|
await expect(
|
||||||
await expect(header).toContainText("Test version");
|
page.locator('script[src="https://www.aeroflot.ru/frontend/static/js/afl-frontend-loader.bundle.js"]'),
|
||||||
|
).toHaveCount(0);
|
||||||
await expect(page.locator("#main-content h1")).toHaveText("Smoke test page");
|
|
||||||
|
|
||||||
const footer = page.getByTestId("standalone-footer");
|
|
||||||
await expect(footer).toBeVisible();
|
|
||||||
await expect(footer).toContainText("Contacts");
|
|
||||||
await expect(footer).toContainText("© Aeroflot 2008-2026");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user