Files
gnezim 60e2149072 Add comprehensive e2e test suites for Tasks 16-25
Tasks 16-20: Online Board Tests (Search/Filter, Tabs, Flight List, Details Modal, Time/Date)
- Task 16: Search & Filter tests (37 tests) - departure/arrival cities, passenger count, cabin class
- Task 17: Arrival/Departure Tabs tests (45 tests) - tab switching, flight display, sorting
- Task 18: Flight List View tests (50 tests) - display, sorting, filtering, pagination, loading states
- Task 19: Flight Details Modal tests (40 tests) - opening/closing, content display, actions
- Task 20: Time & Date Filter tests (43 tests) - date selection, time ranges, calendar navigation

Tasks 21-25: Flight Details Tests (Flight Info, Passengers, Seats, Services, Fares)
- Task 21: Flight Info Display tests (40 tests) - basic info, airports, route visualization, timeline
- Task 22: Passenger Info tests (50 tests) - passenger list, details, services, special requirements
- Task 23: Seat Selection tests (50 tests) - seat map, selection, categories, recommendations
- Task 24: Service Selection tests (25 tests) - baggage, meals, seats, summary
- Task 25: Fare Display tests (55 tests) - fare breakdown, comparisons, discounts, refunds

All tests follow AAA pattern and use data-testid selectors matching Angular version.
Total: 245 tests across 10 feature suites.
2026-04-05 19:25:03 +03:00

261 lines
5.0 KiB
JavaScript
Executable File

import tokenizeAnsi from './tokenize-ansi.js';
function applySgrFragments(activeStyles, fragments) {
for (const fragment of fragments) {
switch (fragment.type) {
case 'reset': {
activeStyles.clear();
break;
}
case 'end': {
activeStyles.delete(fragment.endCode);
break;
}
case 'start': {
activeStyles.delete(fragment.endCode);
activeStyles.set(fragment.endCode, fragment.code);
break;
}
default: {
break;
}
}
}
return activeStyles;
}
function undoAnsiCodes(activeStyles) {
return [...activeStyles.keys()].reverse().join('');
}
function closeHyperlink(hyperlinkToken) {
return `${hyperlinkToken.closePrefix}${hyperlinkToken.terminator}`;
}
function shouldIncludeSgrAfterEnd(token, activeStyles) {
let hasStartFragment = false;
let hasClosingEffect = false;
for (const fragment of token.fragments) {
if (fragment.type === 'start') {
hasStartFragment = true;
continue;
}
if (fragment.type === 'reset' && activeStyles.size > 0) {
hasClosingEffect = true;
continue;
}
if (fragment.type === 'end' && activeStyles.has(fragment.endCode)) {
hasClosingEffect = true;
}
}
return hasClosingEffect && !hasStartFragment;
}
function applySgrToken({token, isPastEnd, activeStyles, returnValue, include, activeHyperlink, position}) {
if (isPastEnd && !shouldIncludeSgrAfterEnd(token, activeStyles)) {
return {
activeStyles,
activeHyperlink,
position,
returnValue,
include,
};
}
activeStyles = applySgrFragments(activeStyles, token.fragments);
if (include) {
returnValue += token.code;
}
return {
activeStyles,
activeHyperlink,
position,
returnValue,
include,
};
}
function applyHyperlinkToken({token, isPastEnd, activeStyles, activeHyperlink, position, returnValue, include}) {
if (
isPastEnd
&& (
token.action !== 'close'
|| !activeHyperlink
)
) {
return {
activeStyles,
activeHyperlink,
position,
returnValue,
include,
};
}
if (token.action === 'open') {
activeHyperlink = token;
} else if (token.action === 'close') {
activeHyperlink = undefined;
}
if (include) {
returnValue += token.code;
}
return {
activeStyles,
activeHyperlink,
position,
returnValue,
include,
};
}
function applyControlToken({token, isPastEnd, activeStyles, activeHyperlink, position, returnValue, include}) {
if (!isPastEnd && include) {
returnValue += token.code;
}
return {
activeStyles,
activeHyperlink,
position,
returnValue,
include,
};
}
function applyCharacterToken({token, start, activeStyles, activeHyperlink, position, returnValue, include}) {
if (
!include
&& position >= start
&& !token.isGraphemeContinuation
) {
include = true;
returnValue = [...activeStyles.values()].join('');
if (activeHyperlink) {
returnValue += activeHyperlink.code;
}
}
if (include) {
returnValue += token.value;
}
position += token.visibleWidth;
return {
activeStyles,
activeHyperlink,
position,
returnValue,
include,
};
}
const tokenHandlers = {
sgr: applySgrToken,
hyperlink: applyHyperlinkToken,
control: applyControlToken,
character: applyCharacterToken,
};
function applyToken(parameters) {
const tokenHandler = tokenHandlers[parameters.token.type];
if (!tokenHandler) {
const {
activeStyles,
activeHyperlink,
position,
returnValue,
include,
} = parameters;
return {
activeStyles,
activeHyperlink,
position,
returnValue,
include,
};
}
return tokenHandler(parameters);
}
function createHasContinuationAheadMap(tokens) {
const hasContinuationAhead = Array.from({length: tokens.length}, () => false);
let nextCharacterIsContinuation = false;
for (let tokenIndex = tokens.length - 1; tokenIndex >= 0; tokenIndex--) {
const token = tokens[tokenIndex];
hasContinuationAhead[tokenIndex] = nextCharacterIsContinuation;
if (token.type === 'character') {
nextCharacterIsContinuation = Boolean(token.isGraphemeContinuation);
}
}
return hasContinuationAhead;
}
export default function sliceAnsi(string, start, end) {
const tokens = tokenizeAnsi(string, {endCharacter: end});
const hasContinuationAhead = createHasContinuationAheadMap(tokens);
let activeStyles = new Map();
let activeHyperlink;
let position = 0;
let returnValue = '';
let include = false;
for (const [tokenIndex, token] of tokens.entries()) {
let isPastEnd = end !== undefined && position >= end;
if (
isPastEnd
&& token.type !== 'character'
&& hasContinuationAhead[tokenIndex]
) {
isPastEnd = false;
}
if (
isPastEnd
&& token.type === 'character'
&& !token.isGraphemeContinuation
) {
break;
}
({activeStyles, activeHyperlink, position, returnValue, include} = applyToken({
token,
isPastEnd,
start,
activeStyles,
activeHyperlink,
position,
returnValue,
include,
}));
}
if (!include) {
return '';
}
if (activeHyperlink) {
returnValue += closeHyperlink(activeHyperlink);
}
// Disable active codes at the end
returnValue += undoAnsiCodes(activeStyles);
return returnValue;
}