From 9a732ecabe4eb4ccee6e60e43551aea5f8a27ef0 Mon Sep 17 00:00:00 2001 From: Tor M Date: Mon, 20 Jan 2025 10:04:20 +0100 Subject: [PATCH] test: performance test with k6 (#480) --- e2e-tests/k6/.gitignore | 4 + e2e-tests/k6/README.md | 36 +++++ e2e-tests/k6/conf/conf.ts | 27 ++++ e2e-tests/k6/conf/env.ts | 15 +++ e2e-tests/k6/conf/options.ts | 35 +++++ e2e-tests/k6/data/locations.ts | 77 +++++++++++ e2e-tests/k6/measurements/measures.ts | 126 ++++++++++++++++++ e2e-tests/k6/measurements/metrics.ts | 76 +++++++++++ e2e-tests/k6/package.json | 8 ++ e2e-tests/k6/pages/assistant.ts | 59 ++++++++ e2e-tests/k6/pages/departures.ts | 23 ++++ e2e-tests/k6/runTest.ts | 20 +++ e2e-tests/k6/scenario/assistant.ts | 66 +++++++++ e2e-tests/k6/scenario/departures.ts | 47 +++++++ e2e-tests/k6/scenario/scenario.ts | 33 +++++ e2e-tests/k6/types/index.ts | 17 +++ e2e-tests/k6/utils/utils.ts | 21 +++ e2e-tests/k6/yarn.lock | 8 ++ src/components/search/search.tsx | 18 ++- src/page-modules/assistant/layout.tsx | 3 + src/page-modules/assistant/trip/index.tsx | 1 + src/page-modules/departures/layout.tsx | 1 + .../departures/stop-place/index.tsx | 6 +- tsconfig.json | 3 +- 24 files changed, 723 insertions(+), 7 deletions(-) create mode 100644 e2e-tests/k6/.gitignore create mode 100644 e2e-tests/k6/README.md create mode 100644 e2e-tests/k6/conf/conf.ts create mode 100644 e2e-tests/k6/conf/env.ts create mode 100644 e2e-tests/k6/conf/options.ts create mode 100644 e2e-tests/k6/data/locations.ts create mode 100644 e2e-tests/k6/measurements/measures.ts create mode 100644 e2e-tests/k6/measurements/metrics.ts create mode 100644 e2e-tests/k6/package.json create mode 100644 e2e-tests/k6/pages/assistant.ts create mode 100644 e2e-tests/k6/pages/departures.ts create mode 100644 e2e-tests/k6/runTest.ts create mode 100644 e2e-tests/k6/scenario/assistant.ts create mode 100644 e2e-tests/k6/scenario/departures.ts create mode 100644 e2e-tests/k6/scenario/scenario.ts create mode 100644 e2e-tests/k6/types/index.ts create mode 100644 e2e-tests/k6/utils/utils.ts create mode 100644 e2e-tests/k6/yarn.lock diff --git a/e2e-tests/k6/.gitignore b/e2e-tests/k6/.gitignore new file mode 100644 index 00000000..de1f94ea --- /dev/null +++ b/e2e-tests/k6/.gitignore @@ -0,0 +1,4 @@ +node_modules +screenshots +logs +./k6 diff --git a/e2e-tests/k6/README.md b/e2e-tests/k6/README.md new file mode 100644 index 00000000..cfb157a4 --- /dev/null +++ b/e2e-tests/k6/README.md @@ -0,0 +1,36 @@ +# Performance tests + +The tests are written in TypeScript and run by the k6 +test tool (https://k6.io/docs/) within a browser context. + +### Run modes + +- Functional test: 1 user and 1 iteration +- Performance test: X iterations over Y users + +See `scenario/scenario.ts` for details. + +### Commands + +Install k6 and the xk6-extension to write to file (error log in `/logs`). On Mac (assume Go is installed along with `GOPATH` is set on `PATH`): +```bash +$ brew install k6 +$ go install go.k6.io/xk6/cmd/xk6@latest +$ xk6 build v0.54.0 --with github.com/avitalique/xk6-file@latest +$ yarn install +``` + +Run functional test + +```bash +# Headless +e2e-tests/k6$ ./k6 run --compatibility-mode=experimental_enhanced runTest.ts -e env=[dev | staging | prod] +# With browser +e2e-tests/k6$ K6_BROWSER_HEADLESS=false ./k6 run --compatibility-mode=experimental_enhanced runTest.ts -e env=[dev | staging | prod] +``` + +Run performance test - default: 100 iterations with 10 users + +```bash +e2e-tests/k6$ ./k6 run --compatibility-mode=experimental_enhanced runTest.ts -e env=[dev | staging | prod] -e performanceTest=true +``` diff --git a/e2e-tests/k6/conf/conf.ts b/e2e-tests/k6/conf/conf.ts new file mode 100644 index 00000000..cbda44b3 --- /dev/null +++ b/e2e-tests/k6/conf/conf.ts @@ -0,0 +1,27 @@ +import { env } from './env.ts'; +import { funcOptions, perfOptions } from './options.ts'; +import { Options } from 'k6/options'; +import { functScenario, perfScenario } from '../scenario/scenario.ts'; +import { Metrics } from '../measurements/metrics.ts'; + +class Conf { + /* @ts-ignore */ + host: string = __ENV.host || env.environments[__ENV.env || 'dev'].host; + + /* @ts-ignore */ + options: Options = + __ENV.performanceTest === 'true' ? perfOptions : funcOptions; + + /* @ts-ignore */ + isPerformanceTest: boolean = __ENV.performanceTest === 'true'; + + /* @ts-ignore */ + usecase = (metrics: Metrics): Promise => { + __ENV.performanceTest === 'true' + ? perfScenario(metrics) + : functScenario(metrics); + }; +} + +// eslint-disable-next-line import/no-anonymous-default-export +export default new Conf(); diff --git a/e2e-tests/k6/conf/env.ts b/e2e-tests/k6/conf/env.ts new file mode 100644 index 00000000..290d6ea8 --- /dev/null +++ b/e2e-tests/k6/conf/env.ts @@ -0,0 +1,15 @@ +import { EnvType } from '../types'; + +export const env: EnvType = { + environments: { + dev: { + host: 'http://localhost:3000', + }, + staging: { + host: 'https://atb-staging.planner-web.mittatb.no', + }, + prod: { + host: 'https://atb.planner-web.mittatb.no', + }, + }, +}; diff --git a/e2e-tests/k6/conf/options.ts b/e2e-tests/k6/conf/options.ts new file mode 100644 index 00000000..9ee41972 --- /dev/null +++ b/e2e-tests/k6/conf/options.ts @@ -0,0 +1,35 @@ +import { Options } from 'k6/options'; + +export const funcOptions: Options = { + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)', 'count'], + scenarios: { + ui: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: {}, +}; + +export const perfOptions: Options = { + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)', 'count'], + scenarios: { + ui: { + executor: 'shared-iterations', + vus: 10, + iterations: 100, + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: {}, +}; diff --git a/e2e-tests/k6/data/locations.ts b/e2e-tests/k6/data/locations.ts new file mode 100644 index 00000000..cc4c2f34 --- /dev/null +++ b/e2e-tests/k6/data/locations.ts @@ -0,0 +1,77 @@ +import { FromLocationType, ToLocationType } from '../types'; + +export const getFromLocation = (): FromLocationType => { + return fromLocations[Math.floor(Math.random() * fromLocations.length)]; +}; + +export const getFromLocationName = (): string => { + return fromLocations[Math.floor(Math.random() * fromLocations.length)].name; +}; + +export const getToLocationName = (): string => { + return toLocations[Math.floor(Math.random() * toLocations.length)].name; +}; + +export const getToLocationRegionName = (): string => { + return toLocationsRegion[Math.floor(Math.random() * toLocationsRegion.length)] + .name; +}; + +const fromLocations: FromLocationType[] = [ + { + name: 'Olav Tryggvasons gate', + quay: '1', + }, + { + name: 'Prinsens gate', + quay: 'P1', + }, + { + name: 'Klettkrysset', + quay: '1', + }, + { + name: 'Dronningens gate', + quay: 'D1', + }, + { + name: 'Tillerterminalen', + quay: '1', + }, +]; + +const toLocations: ToLocationType[] = [ + { + name: 'Melhus sentrum', + }, + { + name: 'Stjørdal stasjon', + }, + { + name: 'Husebytunet', + }, + { + name: 'Lade idrettsanlegg', + }, + { + name: 'Ranheim idrettsplass', + }, +]; + +const toLocationsRegion: ToLocationType[] = [ + { + name: 'Namsos skysstasjon', + }, + { + name: 'Meråker sentrum', + }, + { + name: 'Hitra idrettspark', + }, + { + name: 'Berkåk sentrum', + }, + { + name: 'Selbu skysstasjon', + }, +]; diff --git a/e2e-tests/k6/measurements/measures.ts b/e2e-tests/k6/measurements/measures.ts new file mode 100644 index 00000000..fd1ff1e3 --- /dev/null +++ b/e2e-tests/k6/measurements/measures.ts @@ -0,0 +1,126 @@ +import { Page } from 'k6/browser'; + +export class Measures { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + async mark(label: string) { + switch (label) { + case 'search': + await this.page.evaluate(() => { + window.performance.mark('search'); + }); + break; + case 'search-firstResult': + await this.page.evaluate(() => { + window.performance.mark('search-firstResult'); + }); + break; + case 'search-lastResult': + await this.page.evaluate(() => { + window.performance.mark('search-lastResult'); + }); + break; + case 'assistant-details-open': + await this.page.evaluate(() => { + window.performance.mark('assistant-details-open'); + }); + break; + case 'assistant-details-opened': + await this.page.evaluate(() => { + window.performance.mark('assistant-details-opened'); + }); + break; + case 'departures-details-open': + await this.page.evaluate(() => { + window.performance.mark('departures-details-open'); + }); + break; + case 'departures-details-opened': + await this.page.evaluate(() => { + window.performance.mark('departures-details-opened'); + }); + break; + } + } + + async measure(label: string) { + switch (label) { + case 'measure-search-firstResult': + await this.page.evaluate(() => + window.performance.measure( + 'measure-search-firstResult', + 'search', + 'search-firstResult', + ), + ); + return await this.page.evaluate( + () => + JSON.parse( + JSON.stringify( + window.performance.getEntriesByName( + 'measure-search-firstResult', + ), + ), + )[0].duration, + ); + case 'measure-search-lastResult': + await this.page.evaluate(() => + window.performance.measure( + 'measure-search-lastResult', + 'search', + 'search-lastResult', + ), + ); + return await this.page.evaluate( + () => + JSON.parse( + JSON.stringify( + window.performance.getEntriesByName( + 'measure-search-lastResult', + ), + ), + )[0].duration, + ); + case 'measure-assistant-details-open': + await this.page.evaluate(() => + window.performance.measure( + 'measure-assistant-details-open', + 'assistant-details-open', + 'assistant-details-opened', + ), + ); + return await this.page.evaluate( + () => + JSON.parse( + JSON.stringify( + window.performance.getEntriesByName( + 'measure-assistant-details-open', + ), + ), + )[0].duration, + ); + case 'measure-departures-details-open': + await this.page.evaluate(() => + window.performance.measure( + 'measure-departures-details-open', + 'departures-details-open', + 'departures-details-opened', + ), + ); + return await this.page.evaluate( + () => + JSON.parse( + JSON.stringify( + window.performance.getEntriesByName( + 'measure-departures-details-open', + ), + ), + )[0].duration, + ); + } + } +} diff --git a/e2e-tests/k6/measurements/metrics.ts b/e2e-tests/k6/measurements/metrics.ts new file mode 100644 index 00000000..31f208c4 --- /dev/null +++ b/e2e-tests/k6/measurements/metrics.ts @@ -0,0 +1,76 @@ +import { Trend } from 'k6/metrics'; + +export class Metrics { + private metric_assistant_firstResult: Trend; + private metric_assistant_lastResult: Trend; + private metric_assistant_details_open: Trend; + private metric_assistant_region_firstResult: Trend; + private metric_assistant_region_lastResult: Trend; + private metric_assistant_region_details_open: Trend; + private metric_departures_show: Trend; + private metric_departures_details_open: Trend; + + constructor() { + this.metric_assistant_firstResult = new Trend( + 'metric_assistant_firstResult', + true, + ); + this.metric_assistant_lastResult = new Trend( + 'metric_assistant_lastResult', + true, + ); + this.metric_assistant_details_open = new Trend( + 'metric_assistant_details_open', + true, + ); + this.metric_assistant_region_firstResult = new Trend( + 'metric_assistant_region_firstResult', + true, + ); + this.metric_assistant_region_lastResult = new Trend( + 'metric_assistant_region_lastResult', + true, + ); + this.metric_assistant_region_details_open = new Trend( + 'metric_assistant_region_details_open', + true, + ); + this.metric_departures_show = new Trend('metric_departures_show', true); + this.metric_departures_details_open = new Trend( + 'metric_departures_details_open', + true, + ); + } + + metricAssistantFirstResult(value: number, region: boolean) { + if (region) { + this.metric_assistant_region_firstResult.add(value); + } else { + this.metric_assistant_firstResult.add(value); + } + } + + metricAssistantLastResult(value: number, region: boolean) { + if (region) { + this.metric_assistant_region_lastResult.add(value); + } else { + this.metric_assistant_lastResult.add(value); + } + } + + metricAssistantDetailsOpen(value: number, region: boolean) { + if (region) { + this.metric_assistant_region_details_open.add(value); + } else { + this.metric_assistant_details_open.add(value); + } + } + + metricDeparturesShow(value: number) { + this.metric_departures_show.add(value); + } + + metricDeparturesDetailsOpen(value: number) { + this.metric_departures_details_open.add(value); + } +} diff --git a/e2e-tests/k6/package.json b/e2e-tests/k6/package.json new file mode 100644 index 00000000..8c92c2c9 --- /dev/null +++ b/e2e-tests/k6/package.json @@ -0,0 +1,8 @@ +{ + "name": "planner-web-perf-test", + "version": "0.0.1", + "license": "EUPL-1.2", + "devDependencies": { + "@types/k6": "0.54.2" + } +} diff --git a/e2e-tests/k6/pages/assistant.ts b/e2e-tests/k6/pages/assistant.ts new file mode 100644 index 00000000..ca626acd --- /dev/null +++ b/e2e-tests/k6/pages/assistant.ts @@ -0,0 +1,59 @@ +import { Locator, Page } from 'k6/browser'; + +export class Assistant { + private page: Page; + private searchFromField: Locator; + private searchToField: Locator; + private searchOptionField: Locator; + private firstTrip: Locator; + private loadMoreButton: Locator; + + private verificationMessage: Locator; + + constructor(page: Page) { + this.page = page; + this.searchFromField = page.locator('[data-testid="searchFrom"]'); + this.searchToField = page.locator('[data-testid="searchTo"]'); + this.searchOptionField = page.locator('[data-testid="list-item-0"]'); + //NOTE: If results are not found in first search the locator could be e.g. "tripPattern-1-0" or "tripPattern-2-0" + this.firstTrip = page.locator('[data-testid="tripPattern-0-0"]'); + this.loadMoreButton = page.locator('[data-testid="loadMoreButton"]'); + + this.verificationMessage = page.locator('.row.contact h2'); + } + + async searchFrom(location: string) { + await this.searchFromField.click(); + await this.searchFromField.type(location); + await Promise.all([ + this.page.waitForLoadState(), + this.page.waitForNavigation(), + this.searchOptionField.click(), + ]); + await this.page.waitForTimeout(1000); + + // To avoid an error where the search from is not sent + const inputText = await this.searchFromField.inputValue(); + if (!inputText || !inputText.includes(location)) { + await this.searchFrom(location); + } + } + + async searchTo(location: string) { + await this.searchToField.click(); + await this.searchToField.type(location); + await this.searchOptionField.click(); + } + + getFirstTrip() { + return this.firstTrip; + } + + getLoadMoreButton() { + return this.loadMoreButton; + } + + async getVerificationMessage() { + return this.verificationMessage.innerText(); + } +} diff --git a/e2e-tests/k6/pages/departures.ts b/e2e-tests/k6/pages/departures.ts new file mode 100644 index 00000000..5eddcad5 --- /dev/null +++ b/e2e-tests/k6/pages/departures.ts @@ -0,0 +1,23 @@ +import { Locator, Page } from 'k6/browser'; + +export class Departures { + private page: Page; + private searchFromField: Locator; + private searchOptionField: Locator; + + constructor(page: Page) { + this.page = page; + this.searchFromField = page.locator('[data-testid="searchFrom"]'); + this.searchOptionField = page.locator('[data-testid="list-item-0"]'); + } + + async searchFrom(location: string) { + await this.searchFromField.click(); + await this.searchFromField.type(location); + await this.searchOptionField.click(); + } + + getFirstDeparture(quay: string) { + return this.page.locator(`[data-testid="departure-${quay}-0"]`); + } +} diff --git a/e2e-tests/k6/runTest.ts b/e2e-tests/k6/runTest.ts new file mode 100644 index 00000000..30bf817d --- /dev/null +++ b/e2e-tests/k6/runTest.ts @@ -0,0 +1,20 @@ +import { Metrics } from './measurements/metrics.ts'; +import Conf from './conf/conf.ts'; +import { Options } from 'k6/options'; + +// Options +export const options: Options = Conf.options; + +// Trends +const metrics = new Metrics(); + +//Before the simulation starts +export function setup() { + console.log('---- Setup ----'); + console.log(`Environment: ${Conf.host}`); +} + +// eslint-disable-next-line import/no-anonymous-default-export +export default async function () { + await Conf.usecase(metrics); +} diff --git a/e2e-tests/k6/scenario/assistant.ts b/e2e-tests/k6/scenario/assistant.ts new file mode 100644 index 00000000..fb2bf84a --- /dev/null +++ b/e2e-tests/k6/scenario/assistant.ts @@ -0,0 +1,66 @@ +import { Page } from 'k6/browser'; +import { Assistant } from '../pages/assistant.ts'; +import { Measures } from '../measurements/measures.ts'; +import { Metrics } from '../measurements/metrics.ts'; +import Conf from '../conf/conf.ts'; +import { errorLog, screenshot } from '../utils/utils.ts'; +import { + getFromLocationName, + getToLocationName, + getToLocationRegionName, +} from '../data/locations.ts'; + +export async function assistant( + page: Page, + metrics: Metrics, + region: boolean = false, +) { + try { + await page.goto(`${Conf.host}/assistant`); + const measures = new Measures(page); + const assistant = new Assistant(page); + const fromLocation = getFromLocationName(); + const toLocation = region ? getToLocationRegionName() : getToLocationName(); + + await assistant.searchFrom(fromLocation); + await assistant.searchTo(toLocation); + await measures.mark('search'); + await page.waitForNavigation(); + + const trip = assistant.getFirstTrip(); + await trip.waitFor({ + state: 'visible', + }); + await measures.mark('search-firstResult'); + + const loadMore = assistant.getLoadMoreButton(); + await loadMore.waitFor({ + state: 'visible', + }); + await measures.mark('search-lastResult'); + const searchToFirstResult = await measures.measure( + 'measure-search-firstResult', + ); + const searchToLastResult = await measures.measure( + 'measure-search-lastResult', + ); + + // Open trip details + await trip.click(); + await measures.mark('assistant-details-open'); + await page.waitForNavigation(); + await measures.mark('assistant-details-opened'); + const openTripDetails = await measures.measure( + 'measure-assistant-details-open', + ); + + metrics.metricAssistantFirstResult(searchToFirstResult, region); + metrics.metricAssistantLastResult(searchToLastResult, region); + metrics.metricAssistantDetailsOpen(openTripDetails, region); + + await screenshot(page, 'assistant'); + } catch (e) { + errorLog(`[ERROR] Assistant: ${e}`); + await screenshot(page, 'error_assistant'); + } +} diff --git a/e2e-tests/k6/scenario/departures.ts b/e2e-tests/k6/scenario/departures.ts new file mode 100644 index 00000000..f23e3f99 --- /dev/null +++ b/e2e-tests/k6/scenario/departures.ts @@ -0,0 +1,47 @@ +import { Page } from 'k6/browser'; +import { Measures } from '../measurements/measures.ts'; +import { Metrics } from '../measurements/metrics.ts'; +import { Departures } from '../pages/departures.ts'; +import Conf from '../conf/conf.ts'; +import { errorLog, screenshot } from '../utils/utils.ts'; +import { getFromLocation } from '../data/locations.ts'; +import { FromLocationType } from '../types'; + +export async function departures(page: Page, metrics: Metrics) { + try { + await page.goto(`${Conf.host}/departures`); + const measures = new Measures(page); + const departures = new Departures(page); + + const fromLocation: FromLocationType = getFromLocation(); + const departure = departures.getFirstDeparture(fromLocation.quay); + + await departures.searchFrom(fromLocation.name); + await measures.mark('search'); + await page.waitForNavigation(); + await departure.waitFor({ + state: 'visible', + }); + await measures.mark('search-firstResult'); + const searchToFirstResult = await measures.measure( + 'measure-search-firstResult', + ); + + // Open departure details + await departure.click(); + await measures.mark('departures-details-open'); + await page.waitForNavigation(); + await measures.mark('departures-details-opened'); + const openDepDetails = await measures.measure( + 'measure-departures-details-open', + ); + + metrics.metricDeparturesShow(searchToFirstResult); + metrics.metricDeparturesDetailsOpen(openDepDetails); + + await screenshot(page, 'departures'); + } catch (e) { + errorLog(`[ERROR] Departures: ${e}`); + await screenshot(page, 'error_departures'); + } +} diff --git a/e2e-tests/k6/scenario/scenario.ts b/e2e-tests/k6/scenario/scenario.ts new file mode 100644 index 00000000..75b522e8 --- /dev/null +++ b/e2e-tests/k6/scenario/scenario.ts @@ -0,0 +1,33 @@ +import { browser, Page } from 'k6/browser'; +import { Metrics } from '../measurements/metrics.ts'; +import { assistant } from './assistant.ts'; +import { departures } from './departures.ts'; + +export async function functScenario(metrics: Metrics) { + const page: Page = await browser.newPage(); + try { + await assistant(page, metrics); + await departures(page, metrics); + } finally { + await page.close(); + } +} + +/* + - 75 % search for trips "locally" + - 25 % search for trips to the region + - 100% looks up departures + */ +export async function perfScenario(metrics: Metrics) { + const page: Page = await browser.newPage(); + try { + if (Math.random() > 0.75) { + await assistant(page, metrics, true); + } else { + await assistant(page, metrics); + } + await departures(page, metrics); + } finally { + await page.close(); + } +} diff --git a/e2e-tests/k6/types/index.ts b/e2e-tests/k6/types/index.ts new file mode 100644 index 00000000..ab52a142 --- /dev/null +++ b/e2e-tests/k6/types/index.ts @@ -0,0 +1,17 @@ +export type EnvType = { + environments: { + [environment: string]: { + host: string; + }; + }; +}; + +export type FromLocationType = { + name: string; + quay: string; +}; + +export type ToLocationType = { + name: string; + quay?: string; +}; diff --git a/e2e-tests/k6/utils/utils.ts b/e2e-tests/k6/utils/utils.ts new file mode 100644 index 00000000..9af9ae83 --- /dev/null +++ b/e2e-tests/k6/utils/utils.ts @@ -0,0 +1,21 @@ +import { Page } from 'k6/browser'; +import Conf from '../conf/conf.ts'; +/* @ts-ignore */ +import file from 'k6/x/file'; + +const logFile = 'logs/log.txt'; +const errorLogFile = 'logs/log-error.txt'; + +export const screenshot = async (page: Page, label: string) => { + if (!Conf.isPerformanceTest) { + await page.screenshot({ path: `screenshots/${label}.png` }); + } +}; + +export const log = (label: string) => { + file.appendString(logFile, `${label}\n`); +}; + +export const errorLog = (label: string) => { + file.appendString(errorLogFile, `${label}\n`); +}; diff --git a/e2e-tests/k6/yarn.lock b/e2e-tests/k6/yarn.lock new file mode 100644 index 00000000..9e9c9612 --- /dev/null +++ b/e2e-tests/k6/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/k6@0.54.2": + version "0.54.2" + resolved "https://registry.yarnpkg.com/@types/k6/-/k6-0.54.2.tgz#944d6e20881d0fed3123742654ec8a12e175ea49" + integrity sha512-B5LPxeQm97JnUTpoKNE1UX9jFp+JiJCAXgZOa2P7aChxVoPQXKfWMzK+739xHq3lPkKj1aV+HeOxkP56g/oWBg== diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index a804c440..0135c24a 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -16,6 +16,7 @@ type SearchProps = { initialFeature?: GeocoderFeature; selectedItem?: GeocoderFeature; autocompleteFocusPoint?: GeocoderFeature; + testID?: string; }; export default function Search({ @@ -26,25 +27,30 @@ export default function Search({ initialFeature, selectedItem, autocompleteFocusPoint, + testID, }: SearchProps) { const [query, setQuery] = useState(''); const { data } = useAutocomplete(query, autocompleteFocusPoint); const { t } = useTranslation(); - function getA11yStatusMessage({ isOpen, resultCount, previousResultCount }: A11yStatusMessageOptions) { + function getA11yStatusMessage({ + isOpen, + resultCount, + previousResultCount, + }: A11yStatusMessageOptions) { if (!isOpen) { - return '' + return ''; } if (!resultCount) { - return t(ComponentText.SearchInput.noResults) + return t(ComponentText.SearchInput.noResults); } if (resultCount !== previousResultCount) { - return t(ComponentText.SearchInput.results(resultCount)) + return t(ComponentText.SearchInput.results(resultCount)); } - return '' + return ''; } return ( @@ -82,6 +88,7 @@ export default function Search({ className={style.input} placeholder={placeholder} {...getInputProps()} + data-testid={testID} /> @@ -100,6 +107,7 @@ export default function Search({ index, item, })} + data-testid={`list-item-${index}`} >
diff --git a/src/page-modules/assistant/layout.tsx b/src/page-modules/assistant/layout.tsx index 06f83eac..322c8466 100644 --- a/src/page-modules/assistant/layout.tsx +++ b/src/page-modules/assistant/layout.tsx @@ -156,6 +156,7 @@ function AssistantLayout({ children, tripQuery }: AssistantLayoutProps) { placeholder={t(PageText.Assistant.search.input.placeholder)} onChange={onFromSelected} selectedItem={tripQuery.from ?? undefined} + testID="searchFrom" button={ loadMore()} title={t(PageText.Assistant.trip.fetchMore)} state={isLoadingMore ? 'loading' : undefined} + testID="loadMoreButton" /> )} diff --git a/src/page-modules/departures/layout.tsx b/src/page-modules/departures/layout.tsx index 476aba13..e4c91c23 100644 --- a/src/page-modules/departures/layout.tsx +++ b/src/page-modules/departures/layout.tsx @@ -63,6 +63,7 @@ function DeparturesLayout({ children, fromQuery }: DeparturesLayoutProps) { placeholder={t(PageText.Departures.search.input.placeholder)} selectedItem={fromQuery.from ?? undefined} onChange={onSelectFeature} + testID="searchFrom" button={ 0 ? ( <> - {departures.map((departure) => ( + {departures.map((departure, index) => ( ))} @@ -208,11 +209,13 @@ export function EstimatedCallList({ quay }: EstimatedCallListProps) { type EstimatedCallItemProps = { quayId: string; departure: Departure; + testID?: string; }; export function EstimatedCallItem({ quayId, departure, + testID, }: EstimatedCallItemProps) { const { t } = useTranslation(); const lineName = formatDestinationDisplay(t, departure.destinationDisplay); @@ -221,6 +224,7 @@ export function EstimatedCallItem({
{(departure.transportMode || departure.publicCode) && ( diff --git a/tsconfig.json b/tsconfig.json index ee2d0dca..f1f307e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, + "allowImportingTsExtensions": true, "skipLibCheck": true, "strict": true, "noEmit": true, @@ -24,5 +25,5 @@ "types": ["node", "vite/client"] }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "e2e-tests/k6"] }