From f108a32a22dee0c44307016bb8713870825ece65 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Mon, 26 Jan 2026 19:36:41 +0100 Subject: [PATCH 01/24] fix(tests): improve cookie banner handling and refactor e2e setup - Add global setup for Playwright tests with cookie banner functionality. - Simplify `closeExternalBanners` utility for better production handling. - Skip cookie banner tests in non-production environments. - Update test configurations and remove unnecessary consent banner logic. - Adjust case-study and navigation link verifications. - Amend storage state usage in Playwright settings. # Conflicts: # .gitignore --- .gitignore | 1 + playwright.config.ts | 2 + test/e2e/server-side-use-cases.spec.ts | 2 +- test/e2e/teach/courses.spec.ts | 4 +- test/e2e/teach/education.spec.ts | 4 +- test/e2e/teach/why.spec.ts | 4 +- test/e2e/utils.ts | 18 ++++---- test/e2e/webhelp.spec.ts | 4 +- test/global-setup.ts | 46 +++++++++++++++++++ test/production/api-navigation.spec.ts | 2 - .../community-kotlin-user-groups.spec.ts | 2 - ...ity-overview-keep-in-touch-section.spec.ts | 2 - .../community-overview-rest-buttons.spec.ts | 2 - test/production/cookie-banner.spec.ts | 29 ++++++++++++ .../footer-kotlin-ecosystem-buttons.spec.ts | 2 - .../footer-social-media-buttons.spec.ts | 2 - test/production/global-search.spec.ts | 6 --- test/production/grammar.spec.ts | 5 -- test/production/landings.spec.ts | 15 ++---- test/production/main-page-buttons.spec.ts | 2 - test/production/play-tab.spec.ts | 2 - test/production/solutions-tab.spec.ts | 2 - test/production/teach.spec.ts | 2 - test/utils.ts | 21 ++++----- 24 files changed, 109 insertions(+), 72 deletions(-) create mode 100644 test/global-setup.ts create mode 100644 test/production/cookie-banner.spec.ts diff --git a/.gitignore b/.gitignore index 728327101da..b2e57390bbe 100644 --- a/.gitignore +++ b/.gitignore @@ -49,5 +49,6 @@ generated /reports* /data/page_views_map.json test-results +test/storage-state.json public/data \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts index 2403db5bfff..23d6ff845ec 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,6 +3,7 @@ import { defineConfig, devices } from '@playwright/test'; const MAX_DIFF_PIXEL_RATIO = 0.025 as const; export default defineConfig({ + globalSetup: require.resolve('./test/global-setup.ts'), forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, reporter: process.env.CI ? 'dot' : 'list', @@ -13,6 +14,7 @@ export default defineConfig({ }, use: { baseURL: process.env.BASE_URL || 'http://localhost:9000', + storageState: 'test/storage-state.json', trace: 'off', screenshot: 'only-on-failure', video: 'retain-on-failure' diff --git a/test/e2e/server-side-use-cases.spec.ts b/test/e2e/server-side-use-cases.spec.ts index 5af6c53d53e..0b662b74d31 100644 --- a/test/e2e/server-side-use-cases.spec.ts +++ b/test/e2e/server-side-use-cases.spec.ts @@ -55,7 +55,7 @@ test.describe('Server-Side landing page', async () => { await expect(serverSidePage.heroCaseStudiesLink).toBeVisible(); await serverSidePage.heroCaseStudiesLink.click(); - expect(page.url()).toContain('/lp/server-side/case-studies/'); + expect(page.url()).toContain('/case-studies/'); }); test('Server-side: check Ktor get started link', async ({ page }) => { diff --git a/test/e2e/teach/courses.spec.ts b/test/e2e/teach/courses.spec.ts index a3612181737..202786044f7 100644 --- a/test/e2e/teach/courses.spec.ts +++ b/test/e2e/teach/courses.spec.ts @@ -5,10 +5,10 @@ import { testSelector } from '../../utils'; import { checkTeachCta, checkTeachMap, checkTeachNav } from './utils'; test.describe('Courses page appearance and functionality', async () => { - test.beforeEach(async ({ page, context, baseURL }) => { + test.beforeEach(async ({ page }) => { const coursesPage = new CoursesPage(page); await coursesPage.init(); - await closeExternalBanners(context, page, baseURL); + await closeExternalBanners(page); }); test('Should load the courses page correctly', async ({ page }) => { diff --git a/test/e2e/teach/education.spec.ts b/test/e2e/teach/education.spec.ts index fa1fe172bae..1ee3ba82bf4 100644 --- a/test/e2e/teach/education.spec.ts +++ b/test/e2e/teach/education.spec.ts @@ -5,10 +5,10 @@ import { testSelector } from '../../utils'; import { checkTeachCta, checkTeachMap, checkTeachNav, MAILTO_LINK, MATERIALS_LINK, SIGNUP_LINK } from './utils'; test.describe('Education landing page content and interactions', async () => { - test.beforeEach(async ({ context, page, baseURL }) => { + test.beforeEach(async ({ page }) => { const teachPage = new TeachPage(page); await teachPage.init(); - await closeExternalBanners(context, page, baseURL); + await closeExternalBanners(page); }); test('Should load the education page correctly', async ({ page }) => { diff --git a/test/e2e/teach/why.spec.ts b/test/e2e/teach/why.spec.ts index 58b0e74c36c..2118330e4f8 100644 --- a/test/e2e/teach/why.spec.ts +++ b/test/e2e/teach/why.spec.ts @@ -21,10 +21,10 @@ function toId(label: typeof LIST_OF_SECTION[number][0]) { } test.describe('Why Teach Kotlin page appearance and functionality', async () => { - test.beforeEach(async ({ page, context, baseURL }) => { + test.beforeEach(async ({ page }) => { const whyTeachPage = new WhyTeachPage(page); await whyTeachPage.init(); - await closeExternalBanners(context, page, baseURL); + await closeExternalBanners(page); }); test('Should load the Why Teach Kotlin page correctly', async ({ page }) => { diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts index ac5b2c738de..5355b39741e 100644 --- a/test/e2e/utils.ts +++ b/test/e2e/utils.ts @@ -1,5 +1,5 @@ -import { BrowserContext, ElementHandle, expect, Page, test } from '@playwright/test'; -import { closeCookiesConsentBanner, isSkipScreenshot } from '../utils'; +import { ElementHandle, expect, Page, test } from '@playwright/test'; +import { isProduction, isSkipScreenshot } from '../utils'; import { PageAssertionsToHaveScreenshotOptions } from 'playwright/types/test'; export async function getElementScreenshotWithPadding(page: Page, element: ElementHandle, padding: number): Promise { @@ -18,14 +18,12 @@ export async function getElementScreenshotWithPadding(page: Page, element: Eleme } } -export async function closeExternalBanners(context: BrowserContext, page: Page, baseUrl: string) { - if (baseUrl.startsWith('https://kotlinlang.org/')) { - await closeCookiesConsentBanner(context, baseUrl); - } else { - await page.frameLocator('#webpack-dev-server-client-overlay') - .locator('[aria-label="Dismiss"]') - .click(); - } +export async function closeExternalBanners(page: Page) { + if (isProduction(page.url())) return; + + await page.frameLocator('#webpack-dev-server-client-overlay') + .locator('[aria-label="Dismiss"]') + .click(); } export function pageWrapperMask(page: Page) { diff --git a/test/e2e/webhelp.spec.ts b/test/e2e/webhelp.spec.ts index 3919d2b1e27..892ab42cde9 100644 --- a/test/e2e/webhelp.spec.ts +++ b/test/e2e/webhelp.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { testSelector } from '../utils'; +import { isProduction, testSelector } from '../utils'; import { WebHelpPage } from '../page/webhelp-page'; import { ELEMENT_PADDING_OFFSET, @@ -11,6 +11,8 @@ import { getElementScreenshotWithPadding } from './utils'; import os from 'os'; test.describe('WebHelp page appearance', async () => { + test.skip(({ baseURL }) => isProduction(baseURL), 'Skip tests on production environment'); + test.beforeEach(async ({ page }) => { const webHelpPage = new WebHelpPage(page, '/docs/test-page.html'); await webHelpPage.init(); diff --git a/test/global-setup.ts b/test/global-setup.ts new file mode 100644 index 00000000000..0c4d9942c48 --- /dev/null +++ b/test/global-setup.ts @@ -0,0 +1,46 @@ +import { join } from 'node:path'; +import { chromium, FullConfig } from '@playwright/test'; +import { isProduction } from './utils'; + +export default async function globalSetup(config: FullConfig) { + for (const project of config.projects) { + console.log(`[Global Setup] Processing project ${project.name}`); + const baseURL = project.use?.baseURL; + + if (isProduction(baseURL)) { + const storageStatePath = join(__dirname, 'storage-state.json'); + await closeConsentBanner(baseURL, storageStatePath); + } + } +} + +async function closeConsentBanner(baseURL: string, storageStatePath: string) { + console.log(`[Global Setup] Starting cookie banner setup for ${baseURL}`); + + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + try { + await page.goto(baseURL); + + try { + const acceptButton = page.getByRole('button', { name: 'Accept All' }); + await acceptButton.waitFor({ state: 'visible', timeout: 5000 }); + await acceptButton.click(); + + await page.waitForTimeout(1000); + } catch (error) { + console.log('[Global Setup] Cookie banner not found - continuing'); + } + + await context.storageState({ path: storageStatePath }); + console.log(`[Global Setup] Storage state saved to ${storageStatePath}`); + } catch (error) { + console.error('[Global Setup] Error during setup:', error); + throw error; + } finally { + await context.close(); + await browser.close(); + } +} diff --git a/test/production/api-navigation.spec.ts b/test/production/api-navigation.spec.ts index 814776ca94c..5893b5afd92 100644 --- a/test/production/api-navigation.spec.ts +++ b/test/production/api-navigation.spec.ts @@ -3,8 +3,6 @@ import { expect, Page, test } from '@playwright/test'; test.describe('Api navigation', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); const navbar = page.locator('[data-test="header"]'); const apiButton = navbar.getByText('API', { exact: true }); await expect(apiButton).toBeVisible(); diff --git a/test/production/community-kotlin-user-groups.spec.ts b/test/production/community-kotlin-user-groups.spec.ts index 546454fcc97..6c90b966e11 100644 --- a/test/production/community-kotlin-user-groups.spec.ts +++ b/test/production/community-kotlin-user-groups.spec.ts @@ -3,8 +3,6 @@ import { test, expect } from '@playwright/test'; test.describe('Community Kotlin User Groups page', () => { test.beforeEach(async ({ page }) => { await page.goto('/community/user-groups/'); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); }); test('Overview in navbar opens the related page', async ({ page }) => { diff --git a/test/production/community-overview-keep-in-touch-section.spec.ts b/test/production/community-overview-keep-in-touch-section.spec.ts index 3afe47de229..25b4c8215a7 100644 --- a/test/production/community-overview-keep-in-touch-section.spec.ts +++ b/test/production/community-overview-keep-in-touch-section.spec.ts @@ -7,8 +7,6 @@ test.describe('Community page, overview tab, keep in touch section', () => { test.beforeEach(async ({ page }) => { communityPage = new CommunityPage(page); await communityPage.init(); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); }); test('Slack button opens the related page', async ({ page, context }) => { diff --git a/test/production/community-overview-rest-buttons.spec.ts b/test/production/community-overview-rest-buttons.spec.ts index 9ad519d7952..4b8183e2efc 100644 --- a/test/production/community-overview-rest-buttons.spec.ts +++ b/test/production/community-overview-rest-buttons.spec.ts @@ -3,8 +3,6 @@ import { test, expect } from '@playwright/test'; test.describe('Community page, overview tab, rest buttons', () => { test.beforeEach(async ({ page }) => { await page.goto('/community/'); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); }); test('Kotlin User Groups in navbar opens the user-groups page', async ({ page }) => { diff --git a/test/production/cookie-banner.spec.ts b/test/production/cookie-banner.spec.ts new file mode 100644 index 00000000000..fb7b256445e --- /dev/null +++ b/test/production/cookie-banner.spec.ts @@ -0,0 +1,29 @@ +import { expect, test as base } from '@playwright/test'; +import { isProduction } from '../utils'; + +const test = base.extend({ + context: async function makeCleanContext({ browser }, use) { + const context = await browser.newContext({ + storageState: undefined + }); + await use(context); + await context.close(); + } +}); + +test.skip(({ baseURL }) => !isProduction(baseURL), 'Cookie banner only on production'); + +test.describe('Cookie banner functionality', () => { + test('Cookie banner should be visible and closeable', async ({ page, baseURL }) => { + await page.goto(baseURL || '/'); + + const acceptButton = page.getByRole('button', { name: 'Accept All' }); + await expect(acceptButton).toBeVisible({ timeout: 10000 }); + + await acceptButton.click(); + await expect(acceptButton).toBeHidden({ timeout: 3000 }); + + const cookies = await page.context().cookies(); + expect(cookies.length).toBeGreaterThan(0); + }); +}); diff --git a/test/production/footer-kotlin-ecosystem-buttons.spec.ts b/test/production/footer-kotlin-ecosystem-buttons.spec.ts index 24a477662d0..975445a1d98 100644 --- a/test/production/footer-kotlin-ecosystem-buttons.spec.ts +++ b/test/production/footer-kotlin-ecosystem-buttons.spec.ts @@ -3,8 +3,6 @@ import { test, expect } from '@playwright/test'; test.describe('Footer kotlin ecosystem buttons', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); await page.evaluate(() => { window.scrollTo(0, document.body.scrollHeight); }); diff --git a/test/production/footer-social-media-buttons.spec.ts b/test/production/footer-social-media-buttons.spec.ts index feae1d02c7f..ba83233bae3 100644 --- a/test/production/footer-social-media-buttons.spec.ts +++ b/test/production/footer-social-media-buttons.spec.ts @@ -3,8 +3,6 @@ import { test, expect } from '@playwright/test'; test.describe('Footer social media buttons', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); await page.evaluate(() => { window.scrollTo(0, document.body.scrollHeight); }); diff --git a/test/production/global-search.spec.ts b/test/production/global-search.spec.ts index c2682cd4ea8..8e3cc4a55cf 100644 --- a/test/production/global-search.spec.ts +++ b/test/production/global-search.spec.ts @@ -2,8 +2,6 @@ import { Page, test } from '@playwright/test'; import { IndexPage } from '../page/index-page'; import { CommunityPage } from '../page/community-page'; -import { TeachPage } from '../page/teach/education'; -import { closeCookiesConsentBanner } from '../utils'; const SEARCH_STRING = 'Community'; @@ -25,10 +23,6 @@ const pagesWithGlobalSearch = [ test.describe.configure({ mode: 'parallel' }); test.describe('Global Search Component', async () => { - test.beforeEach(async ({ context, baseURL }) => { - await closeCookiesConsentBanner(context, baseURL); - }); - for (const pageWithGlobalSearch of pagesWithGlobalSearch) { test(`Quick Search on ${pageWithGlobalSearch.name} Page`, async ({ page }) => { diff --git a/test/production/grammar.spec.ts b/test/production/grammar.spec.ts index cb962dc8c70..3e4c0506163 100644 --- a/test/production/grammar.spec.ts +++ b/test/production/grammar.spec.ts @@ -1,12 +1,7 @@ import { expect, test } from '@playwright/test'; -import { closeCookiesConsentBanner, isStaging } from '../utils'; import { GrammarPage } from '../page/grammar-page'; test.describe('Grammar page', () => { - test.beforeEach(async ({ context, baseURL }) => { - await closeCookiesConsentBanner(context, baseURL); - }); - test('Grammar page should be accessible', async ({ page }) => { const grammar = new GrammarPage(page); await grammar.init(); diff --git a/test/production/landings.spec.ts b/test/production/landings.spec.ts index 3877481470b..e29f34b6939 100644 --- a/test/production/landings.spec.ts +++ b/test/production/landings.spec.ts @@ -1,16 +1,12 @@ -import { test, expect } from '@playwright/test'; -import { closeCookiesConsentBanner, isStaging } from '../utils'; +import { expect, test } from '@playwright/test'; +import { isProduction } from '../utils'; test.describe.configure({ mode: 'parallel' }); test.describe('/lp/ pages list', async () => { - test.beforeEach(async ({ context, baseURL }) => { - await closeCookiesConsentBanner(context, baseURL); - }); - - test(`Check /lp/multiplatform default redirects`, async ({ page, baseURL }) => { - test.skip(isStaging(baseURL), 'for host with reverse-proxy only'); + test.skip(({ baseURL }) => !isProduction(baseURL), 'for host with reverse-proxy only'); + test(`Check /lp/multiplatform default redirects`, async ({ page }) => { const targetUrl = 'https://kotlinlang.org/multiplatform/'; await page.goto('/lp/multiplatform'); @@ -23,8 +19,7 @@ test.describe('/lp/ pages list', async () => { expect(page.url()).toEqual(targetUrl); }); - test(`Check /lp/multiplatform case-studies redirect`, async ({ page, baseURL }) => { - test.skip(isStaging(baseURL), 'for host with reverse-proxy only'); + test(`Check /lp/multiplatform case-studies redirect`, async ({ page }) => { const targetUrl = 'https://kotlinlang.org/case-studies/?type=multiplatform'; await page.goto('/lp/multiplatform/case-studies/'); diff --git a/test/production/main-page-buttons.spec.ts b/test/production/main-page-buttons.spec.ts index 90a0397016c..3749ff7b093 100644 --- a/test/production/main-page-buttons.spec.ts +++ b/test/production/main-page-buttons.spec.ts @@ -4,8 +4,6 @@ import { testSelector } from '../utils'; test.describe('Main page buttons', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); }); test('Hero section Get started button', async ({ page }) => { diff --git a/test/production/play-tab.spec.ts b/test/production/play-tab.spec.ts index e8913daf425..cc3b096ef20 100644 --- a/test/production/play-tab.spec.ts +++ b/test/production/play-tab.spec.ts @@ -3,8 +3,6 @@ import { test, expect } from '@playwright/test'; test.describe('Play tab', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); const navbar = page.locator('[data-test="header"]'); const solutionsButton = navbar.getByText('Play').first(); await expect(solutionsButton).toBeVisible(); diff --git a/test/production/solutions-tab.spec.ts b/test/production/solutions-tab.spec.ts index 7ac1febf9e9..07cb35b84dd 100644 --- a/test/production/solutions-tab.spec.ts +++ b/test/production/solutions-tab.spec.ts @@ -4,8 +4,6 @@ import { testSelector } from '../utils'; test.describe('Solutions tab', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); const navbar = page.locator('[data-test="header"]'); const solutionsButton = navbar.getByText('Solutions'); await expect(solutionsButton).toBeVisible(); diff --git a/test/production/teach.spec.ts b/test/production/teach.spec.ts index d7be9bb8db0..f57da43f90e 100644 --- a/test/production/teach.spec.ts +++ b/test/production/teach.spec.ts @@ -3,8 +3,6 @@ import { test, expect } from '@playwright/test'; test.describe('Teach page', () => { test.beforeEach(async ({ page }) => { await page.goto('/education/'); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); }); test('Why teach Kotlin button in navbar opens the related page', async ({ page }) => { diff --git a/test/utils.ts b/test/utils.ts index 69a24c54a28..2246d2ddcce 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -3,19 +3,6 @@ import { PageAssertionsToHaveScreenshotOptions } from 'playwright/types/test'; export const testSelector = (name: string) => `[data-test="${name}"]`; -export function isStaging(baseURL: string): boolean { - const { hostname } = new URL(baseURL); - return hostname !== 'kotlinlang.org'; -} - -export const closeCookiesConsentBanner = async (context: BrowserContext, baseURL: string) => { - const page = await context.newPage(); - await page.goto(baseURL); - await page.waitForSelector('button.ch2-btn.ch2-btn-primary'); - await page.click('button.ch2-btn.ch2-btn-primary'); - await page.close(); -}; - const TRANSITION_TIMEOUT = 2000; export async function checkAnchor(page: Page, anchor: Locator) { @@ -40,3 +27,11 @@ export async function checkScreenshot(element: Locator, options?: PageAssertions ...(options || {}) }); } + +export function isProduction(baseURL: string | undefined) { + try { + return Boolean(baseURL) && new URL(baseURL).hostname === 'kotlinlang.org'; + } catch (error) { + return false; + } +} \ No newline at end of file From 1f4585d49646b77bdaa4b6cb4b5b2f1499316b92 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Mon, 26 Jan 2026 20:45:45 +0100 Subject: [PATCH 02/24] fix(e2e): update Playwright reporter for CI environments - Add `playwright-teamcity-reporter@1.0.5` to dependencies. - Update Playwright configuration to use `playwright-teamcity-reporter` as the default reporter in CI mode. --- package.json | 1 + playwright.config.ts | 2 +- yarn.lock | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7da99728ec5..2060fe7da1b 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "next-optimized-images": "3.0.0-canary.10", "next-transpile-modules": "^10.0.1", "playwright": "1.57.0", + "playwright-teamcity-reporter": "1.0.5", "postcss-custom-media": "10.0.0", "postcss-import": "15.1.0", "prettier": "2.6.2", diff --git a/playwright.config.ts b/playwright.config.ts index 23d6ff845ec..fcf3e28b5a8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ globalSetup: require.resolve('./test/global-setup.ts'), forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - reporter: process.env.CI ? 'dot' : 'list', + reporter: process.env.CI ? 'playwright-teamcity-reporter' : 'list', snapshotDir: 'test/snapshots', expect: { toMatchSnapshot: { maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO }, diff --git a/yarn.lock b/yarn.lock index bf2ffed482d..29e94e6a1ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9254,6 +9254,11 @@ playwright-core@1.57.0: resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.57.0.tgz#3dcc9a865af256fa9f0af0d67fc8dd54eecaebf5" integrity sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ== +playwright-teamcity-reporter@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/playwright-teamcity-reporter/-/playwright-teamcity-reporter-1.0.5.tgz#2598020cff255fd95672a69a266207813bd710da" + integrity sha512-bkFyIxtRkk7Zd0d+EGwqJnr3SapTYMdbGJ2jv6I2ftGJ+nUstBsngxUn2AO5MNkOsYZFAlLDA5QY3GIn+n/Uag== + playwright@1.57.0: version "1.57.0" resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.57.0.tgz#74d1dacff5048dc40bf4676940b1901e18ad0f46" From 2db0ab606c2bfa9571be66d1029f6ba44e65bac1 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Mon, 26 Jan 2026 20:54:06 +0100 Subject: [PATCH 03/24] fix(ci): run all production compatible Playwright tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2060fe7da1b..b51d8f9a41c 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "lint": "next lint", "test": "playwright test test", "test:production": "playwright test test/production", - "test:production:ci": "CI=true BASE_URL=https://kotlinlang.org playwright test test/production", + "test:production:ci": "CI=true BASE_URL=https://kotlinlang.org playwright test", "test:production:headed": "playwright test test/production --headed --project=chromium", "test:production:debug": "PWDEBUG=1 playwright test test/production --project=chromium", "test:e2e": "playwright test test/e2e", From 96e3ad159bd99885b5b2053c995bd9fa1694595c Mon Sep 17 00:00:00 2001 From: zoobestik Date: Mon, 26 Jan 2026 21:21:58 +0100 Subject: [PATCH 04/24] fix(ci): add branch filter for E2E production notifications --- .teamcity/tests/buildTypes/E2EProductionTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/.teamcity/tests/buildTypes/E2EProductionTest.kt b/.teamcity/tests/buildTypes/E2EProductionTest.kt index b6f2b848bf4..797d5e22976 100644 --- a/.teamcity/tests/buildTypes/E2EProductionTest.kt +++ b/.teamcity/tests/buildTypes/E2EProductionTest.kt @@ -48,6 +48,7 @@ object E2EProductionTest : BuildType({ features { notifications { + branchFilter = "+:master" enabled = !isProjectPlayground() notifierSettings = slackNotifier { connection = "PROJECT_EXT_486" From f2b4d243f9f93ab56c1e312def79f8808e42e599 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Mon, 26 Jan 2026 21:30:10 +0100 Subject: [PATCH 05/24] fix(e2e): refactor screenshot verification to use `checkScreenshot` function --- test/e2e/teach/education.spec.ts | 12 ++++++------ test/e2e/teach/utils.ts | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/test/e2e/teach/education.spec.ts b/test/e2e/teach/education.spec.ts index 1ee3ba82bf4..635e3963257 100644 --- a/test/e2e/teach/education.spec.ts +++ b/test/e2e/teach/education.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { TeachPage } from '../../page/teach/education'; import { closeExternalBanners } from '../utils'; -import { testSelector } from '../../utils'; +import { checkScreenshot, testSelector } from '../../utils'; import { checkTeachCta, checkTeachMap, checkTeachNav, MAILTO_LINK, MATERIALS_LINK, SIGNUP_LINK } from './utils'; test.describe('Education landing page content and interactions', async () => { @@ -38,7 +38,7 @@ test.describe('Education landing page content and interactions', async () => { await expect(button).toBeVisible(); await expect(button).toHaveAttribute('href', MATERIALS_LINK); - expect(await block.screenshot()).toMatchSnapshot('launch-course-text.png'); + await checkScreenshot(block); }); test('Should display features section with features', async ({ page }) => { @@ -58,7 +58,7 @@ test.describe('Education landing page content and interactions', async () => { expect(await feature.locator('.ktl-h3').textContent()).toBe(expectedFeatures[i]); } - expect(await featuresSection.screenshot()).toMatchSnapshot('teach-features.png'); + await checkScreenshot(featuresSection); }); test('Should display buttons in top section', async ({ page }) => { @@ -75,7 +75,7 @@ test.describe('Education landing page content and interactions', async () => { await expect(why).toBeVisible(); await expect(why).toHaveAttribute('href', 'why-teach-kotlin.html'); - expect(await block.screenshot()).toMatchSnapshot('teach-top-mobile-buttons.png'); + await checkScreenshot(block); }); test('Should display university statistics correctly', async ({ page }) => { @@ -106,7 +106,7 @@ test.describe('Education landing page content and interactions', async () => { await expect(page.locator('img[alt="Imperial College London"]')).toBeVisible(); await expect(page.locator('img[alt="The University of Chicago"]')).toBeVisible(); - expect(await page.locator('.teach-logos').screenshot()).toMatchSnapshot('teach-logos.png'); + await checkScreenshot(page.locator('.teach-logos')); }); test('Should have a working interactive map', async ({ page }) => { @@ -190,7 +190,7 @@ test.describe('Education landing page content and interactions', async () => { // Verify that the form shows the submitted state (check icon appears) await expect(subscriptionForm.locator('.teach-subscription-form__submitted-icon')).toBeVisible(); - expect(await subscriptionForm.screenshot()).toMatchSnapshot('subscription-form.png'); + await checkScreenshot(subscriptionForm); }); test('Should have a working YouTube player', async ({ page }) => { diff --git a/test/e2e/teach/utils.ts b/test/e2e/teach/utils.ts index 8cb503f479a..f464568c651 100644 --- a/test/e2e/teach/utils.ts +++ b/test/e2e/teach/utils.ts @@ -1,4 +1,5 @@ import { expect, Locator, Page } from '@playwright/test'; +import { checkScreenshot } from '../../utils'; export const MAILTO_LINK = 'mailto:education@kotlinlang.org' as const; export const SIGNUP_LINK = 'https://surveys.jetbrains.com/s3/kotlin-slack-signup-educators' as const; @@ -37,7 +38,7 @@ export async function checkTeachNav(page: Page, selected: typeof NAV_LINKS[numbe await expect(subLink).toHaveAttribute('href', link); } - expect(await navBar.screenshot()).toMatchSnapshot('sticky-menu.png'); + await checkScreenshot(navBar); } export async function checkTeachMap(page: Page, map: Locator) { @@ -98,5 +99,5 @@ export async function checkTeachCta({ page }) { await expect(eduLink).toBeVisible(); await expect(eduLink).toHaveAttribute('href', MAILTO_LINK); - expect(await connectUs.screenshot()).toMatchSnapshot('connect-us.png'); + await checkScreenshot(connectUs); } From bfabe748088490aa8e56d7772e35cf3d74e9acca Mon Sep 17 00:00:00 2001 From: zoobestik Date: Mon, 26 Jan 2026 22:57:13 +0100 Subject: [PATCH 06/24] fix(e2e): refine href matching for tooling section links in why.spec.ts --- test/e2e/teach/why.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/e2e/teach/why.spec.ts b/test/e2e/teach/why.spec.ts index 2118330e4f8..e24177c679f 100644 --- a/test/e2e/teach/why.spec.ts +++ b/test/e2e/teach/why.spec.ts @@ -219,23 +219,23 @@ test.describe('Why Teach Kotlin page appearance and functionality', async () => const description = toolingSectionInfo.locator('.ktl-dimmed-text'); await expect(description).not.toBeVisible(); - const link1 = toolingSectionInfo.locator('a[href="https://www.jetbrains.com/community/education/#students"]'); + const link1 = toolingSectionInfo.locator('a[href^="https://www.jetbrains.com/community/education/#students"]'); await expect(link1).toBeVisible(); expect(await link1.textContent()).toBe('Free IntelliJ IDEA Ultimate license ↗'); - const link2 = toolingSectionInfo.locator('a[href="https://play.kotlinlang.org/"]'); + const link2 = toolingSectionInfo.locator('a[href^="https://play.kotlinlang.org/"]'); await expect(link2).toBeVisible(); expect(await link2.textContent()).toBe('Playground ↗'); - const link3 = toolingSectionInfo.locator('a[href="https://plugins.jetbrains.com/plugin/10081-jetbrains-academy"]'); + const link3 = toolingSectionInfo.locator('a[href^="https://plugins.jetbrains.com/plugin/10081-jetbrains-academy"]'); await expect(link3).toBeVisible(); expect(await link3.textContent()).toBe('JetBrains Academy plugin ↗'); - const link4 = toolingSectionInfo.locator('a[href="https://www.jetbrains.com/code-with-me/"]'); + const link4 = toolingSectionInfo.locator('a[href^="https://www.jetbrains.com/code-with-me/"]'); await expect(link4).toBeVisible(); expect(await link4.textContent()).toBe('Code With Me ↗'); - const link5 = toolingSectionInfo.locator('a[href="https://hyperskill.org/tracks?category=4&utm_source=jbkotlin_hs&utm_medium=referral&utm_campaign=kotlinlang-education&utm_content=button_1&utm_term=22.03.23&"]'); + const link5 = toolingSectionInfo.locator('a[href^="https://hyperskill.org/tracks?category=4&utm_source=jbkotlin_hs&utm_medium=referral&utm_campaign=kotlinlang-education&utm_content=button_1&utm_term=22.03.23&"]'); await expect(link5).toBeVisible(); expect(await link5.textContent()).toBe('Kotlin tracks by JetBrains Academy ↗'); }); From d35cf66b4f39a24e26c285abaf452a8bab7aa879 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Mon, 26 Jan 2026 23:29:33 +0100 Subject: [PATCH 07/24] fix(e2e): adjust CI command to include all Playwright tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b51d8f9a41c..f937a6ead11 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "test:production:headed": "playwright test test/production --headed --project=chromium", "test:production:debug": "PWDEBUG=1 playwright test test/production --project=chromium", "test:e2e": "playwright test test/e2e", - "test:e2e:ci": "CI=true playwright test test/e2e", + "test:e2e:ci": "CI=true playwright test", "test:e2e:headed": "playwright test test/e2e --headed", "test:e2e:debug": "PWDEBUG=1 playwright test test/e2e --project=chromium", "test:e2e:update": "playwright test test/e2e --update-snapshots", From fdad9f53c33609608cb4695b564995deccf78750 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Mon, 26 Jan 2026 23:38:27 +0100 Subject: [PATCH 08/24] fix(ci): streamline E2E test dependencies and Docker setup - Replace obsolete dependencies in E2E tests with simplified `BuildSitePages`. - Update artifact rules to consolidate output in `dist` directory. - Simplify Dockerfile by removing unused `libs` and `_assets` copies. --- .teamcity/tests/buildTypes/E2ETests.kt | 41 +++----------------------- dockerfiles/nginx-server/Dockerfile | 2 -- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/.teamcity/tests/buildTypes/E2ETests.kt b/.teamcity/tests/buildTypes/E2ETests.kt index 2a67494084a..feb3acb4cb1 100644 --- a/.teamcity/tests/buildTypes/E2ETests.kt +++ b/.teamcity/tests/buildTypes/E2ETests.kt @@ -1,12 +1,9 @@ package tests.buildTypes -import documentation.builds.KotlinWithCoroutines import jetbrains.buildServer.configs.kotlin.BuildType import jetbrains.buildServer.configs.kotlin.FailureAction import jetbrains.buildServer.configs.kotlin.buildSteps.script -import kotlinlang.builds.BuildJsAssets -import references.builds.kotlinx.coroutines.KotlinxCoroutinesBuildApiReference -import references.builds.kotlinx.serialization.KotlinxSerializationBuildApiReference +import kotlinlang.builds.BuildSitePages object E2ETests : BuildType({ @@ -21,44 +18,14 @@ object E2ETests : BuildType({ } dependencies { - artifacts(KotlinWithCoroutines) { - cleanDestination = true - artifactRules = """ - +:webHelpImages.zip!** => dist/docs/images/ - +:webHelpKR2.zip!** => dist/docs/ - """.trimIndent() - } - - dependency(BuildJsAssets) { + dependency(BuildSitePages) { snapshot { onDependencyFailure = FailureAction.FAIL_TO_START onDependencyCancel = FailureAction.CANCEL } - - artifacts { - artifactRules = "+:assets.zip!** => _assets/" - } - } - - dependency(KotlinxCoroutinesBuildApiReference) { - snapshot { - onDependencyFailure = FailureAction.CANCEL - onDependencyCancel = FailureAction.CANCEL - } - - artifacts { - artifactRules = "+:pages.zip!** => libs/kotlinx.coroutines/" - } - } - - dependency(KotlinxSerializationBuildApiReference) { - snapshot { - onDependencyFailure = FailureAction.CANCEL - onDependencyCancel = FailureAction.CANCEL - } - artifacts { - artifactRules = "+:pages.zip!** => libs/kotlinx.serialization/" + buildRule = sameChainOrLastFinished() + artifactRules = "+:pages.zip!** => dist/" } } } diff --git a/dockerfiles/nginx-server/Dockerfile b/dockerfiles/nginx-server/Dockerfile index 098d448db6e..5acb09fc224 100644 --- a/dockerfiles/nginx-server/Dockerfile +++ b/dockerfiles/nginx-server/Dockerfile @@ -2,7 +2,5 @@ FROM nginx:latest WORKDIR /var/www COPY dist /usr/share/nginx/html -COPY libs /usr/share/nginx/html/api -COPY _assets /usr/share/nginx/html/_assets RUN chmod -R 755 /usr/share/nginx/html From 7eccac3f7c3c3232d796a5d01204f9d9e70e7c5e Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 00:54:21 +0100 Subject: [PATCH 09/24] fix(e2e): update Playwright version and simplify e2e Docker setup - Upgrade Playwright to version 1.57 in Docker and dependencies. - Replace custom Dockerfiles with official lightweight images. - Simplify `docker-compose-e2e-statics.yml` by using shared volumes and updated networks. - Refactor `global-setup.ts` to ensure consistent storage state initialization. - Adjust `closeExternalBanners` logic to improve development checks. - Fix test `beforeEach` hooks ordering in education-related specs for consistency. --- .teamcity/tests/buildTypes/E2ETests.kt | 36 ++++++++++++-------------- docker-compose-e2e-statics.yml | 34 +++++++++++------------- dockerfiles/e2e-tests/Dockerfile | 2 +- dockerfiles/nginx-server/Dockerfile | 6 ----- dockerfiles/playwright/Dockerfile | 13 ---------- scripts/test/run-e2e-tests.sh | 0 test/e2e/teach/courses.spec.ts | 2 +- test/e2e/teach/education.spec.ts | 2 +- test/e2e/teach/why.spec.ts | 2 +- test/e2e/utils.ts | 4 +-- test/global-setup.ts | 5 +++- test/utils.ts | 6 ++++- 12 files changed, 46 insertions(+), 66 deletions(-) delete mode 100644 dockerfiles/nginx-server/Dockerfile delete mode 100644 dockerfiles/playwright/Dockerfile mode change 100644 => 100755 scripts/test/run-e2e-tests.sh diff --git a/.teamcity/tests/buildTypes/E2ETests.kt b/.teamcity/tests/buildTypes/E2ETests.kt index feb3acb4cb1..e7547d32444 100644 --- a/.teamcity/tests/buildTypes/E2ETests.kt +++ b/.teamcity/tests/buildTypes/E2ETests.kt @@ -13,10 +13,26 @@ object E2ETests : BuildType({ +:test-results/* => test-results.zip """.trimIndent() + requirements { + exists("docker.server.version") + contains("docker.server.osType", "linux") + } + vcs { root(vcsRoots.KotlinLangOrg) } + params { + param("env.WEBTEAM_UI_NPM_TOKEN", "%WEBTEAM_UI_NPM_TOKEN%") + } + + steps { + script { + name = "Run E2E tests" + scriptContent = "./scripts/test/run-e2e-tests.sh" + } + } + dependencies { dependency(BuildSitePages) { snapshot { @@ -29,24 +45,4 @@ object E2ETests : BuildType({ } } } - - steps { - script { - name = "Set execute permissions" - scriptContent = "chmod +x ./scripts/test/run-e2e-tests.sh" - } - script { - name = "Run E2E tests" - scriptContent = "./scripts/test/run-e2e-tests.sh" - } - } - - artifactRules = """ - +:test-results/ => test-results/ - """.trimIndent() - - requirements { - exists("docker.server.version") - contains("docker.server.osType", "linux") - } }) diff --git a/docker-compose-e2e-statics.yml b/docker-compose-e2e-statics.yml index dbc43b8e4ae..e9cb0c22c70 100644 --- a/docker-compose-e2e-statics.yml +++ b/docker-compose-e2e-statics.yml @@ -1,34 +1,30 @@ -version: '3' services: nginx-server: - build: - context: . - dockerfile: ./dockerfiles/nginx-server/Dockerfile - ports: - - 8081:80 + image: nginx:stable-alpine networks: - - network1 + - test-network + volumes: + - ./dist:/usr/share/nginx/html healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost" ] - interval: 30s - timeout: 10s - retries: 3 + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost" ] + interval: 30s + timeout: 5s + retries: 3 playwright: - build: - context: . - dockerfile: ./dockerfiles/playwright/Dockerfile - command: yarn run test:e2e:ci + image: mcr.microsoft.com/playwright:v1.57.0 environment: BASE_URL: "http://nginx-server" + WEBTEAM_UI_NPM_TOKEN: ${WEBTEAM_UI_NPM_TOKEN} + working_dir: /var/www volumes: - - ./test/snapshots:/var/www/test/snapshots - - ./test-results:/var/www/test-results + - .:/var/www + command: sh -c "yarn install && yarn run test:e2e:ci" depends_on: nginx-server: condition: service_healthy networks: - - network1 + - test-network networks: - network1: + test-network: diff --git a/dockerfiles/e2e-tests/Dockerfile b/dockerfiles/e2e-tests/Dockerfile index ef90f1d9dd3..27c9f279f66 100644 --- a/dockerfiles/e2e-tests/Dockerfile +++ b/dockerfiles/e2e-tests/Dockerfile @@ -15,7 +15,7 @@ RUN npm install --global yarn # Install Playwright dependencies RUN apt-get -y install wget gnupg -RUN npx playwright@1.53.0 install-deps +RUN npx playwright@1.57 install-deps WORKDIR /var/www diff --git a/dockerfiles/nginx-server/Dockerfile b/dockerfiles/nginx-server/Dockerfile deleted file mode 100644 index 5acb09fc224..00000000000 --- a/dockerfiles/nginx-server/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM nginx:latest - -WORKDIR /var/www -COPY dist /usr/share/nginx/html - -RUN chmod -R 755 /usr/share/nginx/html diff --git a/dockerfiles/playwright/Dockerfile b/dockerfiles/playwright/Dockerfile deleted file mode 100644 index 3f5d124c5e6..00000000000 --- a/dockerfiles/playwright/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM mcr.microsoft.com/playwright:v1.53.0-noble - -WORKDIR /var/www - -COPY package.json yarn.lock ./ -RUN apt-get update && apt-get install -y build-essential -RUN yarn install --frozen-lockfile - -COPY playwright.config.js . - -COPY . . - -CMD ["tail", "-f"] diff --git a/scripts/test/run-e2e-tests.sh b/scripts/test/run-e2e-tests.sh old mode 100644 new mode 100755 diff --git a/test/e2e/teach/courses.spec.ts b/test/e2e/teach/courses.spec.ts index 202786044f7..2c157388cbe 100644 --- a/test/e2e/teach/courses.spec.ts +++ b/test/e2e/teach/courses.spec.ts @@ -6,9 +6,9 @@ import { checkTeachCta, checkTeachMap, checkTeachNav } from './utils'; test.describe('Courses page appearance and functionality', async () => { test.beforeEach(async ({ page }) => { + await closeExternalBanners(page); const coursesPage = new CoursesPage(page); await coursesPage.init(); - await closeExternalBanners(page); }); test('Should load the courses page correctly', async ({ page }) => { diff --git a/test/e2e/teach/education.spec.ts b/test/e2e/teach/education.spec.ts index 635e3963257..10d61488c76 100644 --- a/test/e2e/teach/education.spec.ts +++ b/test/e2e/teach/education.spec.ts @@ -6,9 +6,9 @@ import { checkTeachCta, checkTeachMap, checkTeachNav, MAILTO_LINK, MATERIALS_LIN test.describe('Education landing page content and interactions', async () => { test.beforeEach(async ({ page }) => { + await closeExternalBanners(page); const teachPage = new TeachPage(page); await teachPage.init(); - await closeExternalBanners(page); }); test('Should load the education page correctly', async ({ page }) => { diff --git a/test/e2e/teach/why.spec.ts b/test/e2e/teach/why.spec.ts index e24177c679f..740814d2417 100644 --- a/test/e2e/teach/why.spec.ts +++ b/test/e2e/teach/why.spec.ts @@ -22,9 +22,9 @@ function toId(label: typeof LIST_OF_SECTION[number][0]) { test.describe('Why Teach Kotlin page appearance and functionality', async () => { test.beforeEach(async ({ page }) => { + await closeExternalBanners(page); const whyTeachPage = new WhyTeachPage(page); await whyTeachPage.init(); - await closeExternalBanners(page); }); test('Should load the Why Teach Kotlin page correctly', async ({ page }) => { diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts index 5355b39741e..932a1901316 100644 --- a/test/e2e/utils.ts +++ b/test/e2e/utils.ts @@ -1,5 +1,5 @@ import { ElementHandle, expect, Page, test } from '@playwright/test'; -import { isProduction, isSkipScreenshot } from '../utils'; +import { isDevelopment, isProduction, isSkipScreenshot } from '../utils'; import { PageAssertionsToHaveScreenshotOptions } from 'playwright/types/test'; export async function getElementScreenshotWithPadding(page: Page, element: ElementHandle, padding: number): Promise { @@ -19,7 +19,7 @@ export async function getElementScreenshotWithPadding(page: Page, element: Eleme } export async function closeExternalBanners(page: Page) { - if (isProduction(page.url())) return; + if (!isDevelopment(page.url())) return; await page.frameLocator('#webpack-dev-server-client-overlay') .locator('[aria-label="Dismiss"]') diff --git a/test/global-setup.ts b/test/global-setup.ts index 0c4d9942c48..bac79a26f46 100644 --- a/test/global-setup.ts +++ b/test/global-setup.ts @@ -1,14 +1,17 @@ import { join } from 'node:path'; +import { writeFile } from 'node:fs/promises'; import { chromium, FullConfig } from '@playwright/test'; import { isProduction } from './utils'; export default async function globalSetup(config: FullConfig) { + const storageStatePath = join(__dirname, 'storage-state.json'); + await writeFile(storageStatePath, '{}', 'utf-8'); + for (const project of config.projects) { console.log(`[Global Setup] Processing project ${project.name}`); const baseURL = project.use?.baseURL; if (isProduction(baseURL)) { - const storageStatePath = join(__dirname, 'storage-state.json'); await closeConsentBanner(baseURL, storageStatePath); } } diff --git a/test/utils.ts b/test/utils.ts index 2246d2ddcce..9f779f89324 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,4 +1,4 @@ -import { BrowserContext, expect, Locator, Page } from '@playwright/test'; +import { expect, Locator, Page } from '@playwright/test'; import { PageAssertionsToHaveScreenshotOptions } from 'playwright/types/test'; export const testSelector = (name: string) => `[data-test="${name}"]`; @@ -34,4 +34,8 @@ export function isProduction(baseURL: string | undefined) { } catch (error) { return false; } +} + +export function isDevelopment(baseURL: string | undefined) { + return !isProduction(baseURL) && process.env.NODE_ENV === 'development'; } \ No newline at end of file From 5e2d6a1b78baacf0429263f32ad0fa290d416417 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 01:39:43 +0100 Subject: [PATCH 10/24] fix(e2e): update baseURL references in landings.spec.ts for consistency with production setup --- test/production/landings.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/production/landings.spec.ts b/test/production/landings.spec.ts index e29f34b6939..f18915b1c6c 100644 --- a/test/production/landings.spec.ts +++ b/test/production/landings.spec.ts @@ -6,8 +6,8 @@ test.describe.configure({ mode: 'parallel' }); test.describe('/lp/ pages list', async () => { test.skip(({ baseURL }) => !isProduction(baseURL), 'for host with reverse-proxy only'); - test(`Check /lp/multiplatform default redirects`, async ({ page }) => { - const targetUrl = 'https://kotlinlang.org/multiplatform/'; + test(`Check /lp/multiplatform default redirects`, async ({ page, baseURL }) => { + const targetUrl = `${baseURL}/multiplatform/`; await page.goto('/lp/multiplatform'); expect(page.url()).toEqual(targetUrl); @@ -19,8 +19,8 @@ test.describe('/lp/ pages list', async () => { expect(page.url()).toEqual(targetUrl); }); - test(`Check /lp/multiplatform case-studies redirect`, async ({ page }) => { - const targetUrl = 'https://kotlinlang.org/case-studies/?type=multiplatform'; + test(`Check /lp/multiplatform case-studies redirect`, async ({ page, baseURL }) => { + const targetUrl = `${baseURL}/case-studies/?type=multiplatform`; await page.goto('/lp/multiplatform/case-studies/'); expect(page.url()).toEqual(targetUrl); From e22cb94190ef2d6f49ca96935405b1a43d3127f9 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 01:48:17 +0100 Subject: [PATCH 11/24] fix(e2e): refine href matching for tooling section links in why.spec.ts --- test/e2e/teach/why.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/teach/why.spec.ts b/test/e2e/teach/why.spec.ts index 740814d2417..973a25d7b4d 100644 --- a/test/e2e/teach/why.spec.ts +++ b/test/e2e/teach/why.spec.ts @@ -235,7 +235,7 @@ test.describe('Why Teach Kotlin page appearance and functionality', async () => await expect(link4).toBeVisible(); expect(await link4.textContent()).toBe('Code With Me ↗'); - const link5 = toolingSectionInfo.locator('a[href^="https://hyperskill.org/tracks?category=4&utm_source=jbkotlin_hs&utm_medium=referral&utm_campaign=kotlinlang-education&utm_content=button_1&utm_term=22.03.23&"]'); + const link5 = toolingSectionInfo.locator('a[href^="https://hyperskill.org/tracks?category=4&utm_source=jbkotlin_hs&utm_medium=referral&utm_campaign=kotlinlang-education&utm_content=button_1&utm_term=22.03.23"]'); await expect(link5).toBeVisible(); expect(await link5.textContent()).toBe('Kotlin tracks by JetBrains Academy ↗'); }); From 5d46b6e736ce47534bb8a051cbdc24c49f698c06 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 02:03:45 +0100 Subject: [PATCH 12/24] fix(e2e): handle load state exception in server-side-use-cases.spec.ts - Add error handling for `waitForLoadState` to improve debugging of failed customer link loads. - Remove unused `isProduction` import from test utilities. --- test/e2e/server-side-use-cases.spec.ts | 6 +++++- test/e2e/utils.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/e2e/server-side-use-cases.spec.ts b/test/e2e/server-side-use-cases.spec.ts index 0b662b74d31..42059457a32 100644 --- a/test/e2e/server-side-use-cases.spec.ts +++ b/test/e2e/server-side-use-cases.spec.ts @@ -107,7 +107,11 @@ test.describe('Server-Side landing page', async () => { customerLink.click() ]); - await newPage.waitForLoadState(); + try { + await newPage.waitForLoadState(); + } catch (error) { + throw new Error(`Failed to load customer link: ${customerLinkURL}\n${error.message}`); + } const originalDomain = new URL(customerLinkURL).hostname; const finalDomain = new URL(newPage.url()).hostname; diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts index 932a1901316..2490a4b3401 100644 --- a/test/e2e/utils.ts +++ b/test/e2e/utils.ts @@ -1,5 +1,5 @@ import { ElementHandle, expect, Page, test } from '@playwright/test'; -import { isDevelopment, isProduction, isSkipScreenshot } from '../utils'; +import { isDevelopment, isSkipScreenshot } from '../utils'; import { PageAssertionsToHaveScreenshotOptions } from 'playwright/types/test'; export async function getElementScreenshotWithPadding(page: Page, element: ElementHandle, padding: number): Promise { From 3756551dcbef883719f537ef98448c426616ddd6 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 02:17:17 +0100 Subject: [PATCH 13/24] fix(e2e): improve customer link interaction in server-side-use-cases.spec.ts - Add `scrollIntoViewIfNeeded` to ensure visibility before hovering customer links. --- test/e2e/server-side-use-cases.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/server-side-use-cases.spec.ts b/test/e2e/server-side-use-cases.spec.ts index 42059457a32..4f04799a50c 100644 --- a/test/e2e/server-side-use-cases.spec.ts +++ b/test/e2e/server-side-use-cases.spec.ts @@ -98,6 +98,7 @@ test.describe('Server-Side landing page', async () => { const customerLinks = await serverSidePage.customersLink.all(); for (const customerLink of customerLinks) { + await customerLink.scrollIntoViewIfNeeded(); await serverSidePage.customersBlock.hover(); const customerLinkURL = await customerLink.getAttribute('href'); From 60e57cad7288d44670eca84530a856456a26332e Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 02:27:31 +0100 Subject: [PATCH 14/24] fix(e2e): refine customer link testing with improved steps - Add context-aware `test.step` usage to enhance customer link verification flow. - Refactor `scrollIntoViewIfNeeded` and `click` logic within `isVisible` check for clarity. - Update `waitForLoadState` with `domcontentloaded` and timeout for better performance. --- test/e2e/server-side-use-cases.spec.ts | 40 ++++++++++++++------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/test/e2e/server-side-use-cases.spec.ts b/test/e2e/server-side-use-cases.spec.ts index 4f04799a50c..0840a9f7cb2 100644 --- a/test/e2e/server-side-use-cases.spec.ts +++ b/test/e2e/server-side-use-cases.spec.ts @@ -98,31 +98,33 @@ test.describe('Server-Side landing page', async () => { const customerLinks = await serverSidePage.customersLink.all(); for (const customerLink of customerLinks) { - await customerLink.scrollIntoViewIfNeeded(); - await serverSidePage.customersBlock.hover(); const customerLinkURL = await customerLink.getAttribute('href'); - if (await customerLink.isVisible()) { - const [newPage] = await Promise.all([ - page.context().waitForEvent('page'), - customerLink.click() - ]); + await test.step(`Check customer link: ${customerLinkURL}`, async () => { + if (await customerLink.isVisible()) { + await customerLink.scrollIntoViewIfNeeded(); - try { - await newPage.waitForLoadState(); - } catch (error) { - throw new Error(`Failed to load customer link: ${customerLinkURL}\n${error.message}`); - } + const [newPage] = await Promise.all([ + page.context().waitForEvent('page'), + customerLink.click() + ]); - const originalDomain = new URL(customerLinkURL).hostname; - const finalDomain = new URL(newPage.url()).hostname; + try { + await newPage.waitForLoadState('domcontentloaded', { timeout: 10000 }); + } catch (error) { + throw new Error(`Failed to load customer link: ${customerLinkURL}\n${error.message}`); + } - if (originalDomain === finalDomain) { - expect(newPage.url()).toContain(customerLinkURL); - } + const originalDomain = new URL(customerLinkURL).hostname; + const finalDomain = new URL(newPage.url()).hostname; - await newPage.close(); - } + if (originalDomain === finalDomain) { + expect(newPage.url()).toContain(customerLinkURL); + } + + await newPage.close(); + } + }); } }); From aa66b570a8a3dc35068ffe4a837ad5734ccb73e2 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 14:15:10 +0100 Subject: [PATCH 15/24] fix(test): refine isDevelopment utility function for URL validation - Add `try-catch` block to handle invalid URL parsing gracefully. - Update development environment check to validate `baseURL` and exclude `localhost`. --- test/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/utils.ts b/test/utils.ts index 9f779f89324..e6996bd056c 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -37,5 +37,9 @@ export function isProduction(baseURL: string | undefined) { } export function isDevelopment(baseURL: string | undefined) { - return !isProduction(baseURL) && process.env.NODE_ENV === 'development'; + try { + return Boolean(baseURL) && new URL(baseURL).hostname !== 'localhost'; + } catch (error) { + return false; + } } \ No newline at end of file From 6f0f9e6eb851eaad6e96f5ffa6d8093e03d3c6eb Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 14:33:20 +0100 Subject: [PATCH 16/24] fix(e2e): update URL assertions for consistency with relative paths - Replace absolute URLs with relative path checks across multiple test specs. - Update `solutions-tab.spec.ts`, `main-page-buttons.spec.ts`, and `landings.spec.ts` to improve maintainability and alignment with production setup. --- test/production/landings.spec.ts | 4 ++-- test/production/main-page-buttons.spec.ts | 2 +- test/production/solutions-tab.spec.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/production/landings.spec.ts b/test/production/landings.spec.ts index f18915b1c6c..4009b38f756 100644 --- a/test/production/landings.spec.ts +++ b/test/production/landings.spec.ts @@ -7,7 +7,7 @@ test.describe('/lp/ pages list', async () => { test.skip(({ baseURL }) => !isProduction(baseURL), 'for host with reverse-proxy only'); test(`Check /lp/multiplatform default redirects`, async ({ page, baseURL }) => { - const targetUrl = `${baseURL}/multiplatform/`; + const targetUrl = `/multiplatform/`; await page.goto('/lp/multiplatform'); expect(page.url()).toEqual(targetUrl); @@ -20,7 +20,7 @@ test.describe('/lp/ pages list', async () => { }); test(`Check /lp/multiplatform case-studies redirect`, async ({ page, baseURL }) => { - const targetUrl = `${baseURL}/case-studies/?type=multiplatform`; + const targetUrl = `/case-studies/?type=multiplatform`; await page.goto('/lp/multiplatform/case-studies/'); expect(page.url()).toEqual(targetUrl); diff --git a/test/production/main-page-buttons.spec.ts b/test/production/main-page-buttons.spec.ts index 3749ff7b093..92cc6cf7312 100644 --- a/test/production/main-page-buttons.spec.ts +++ b/test/production/main-page-buttons.spec.ts @@ -47,7 +47,7 @@ test.describe('Main page buttons', () => { const multiplatformButton = page.getByTestId('highlighted-cases-section').getByRole('link', { name: 'Learn about Kotlin Multiplatform' }); await expect(multiplatformButton).toBeVisible(); await multiplatformButton.click(); - await expect(page.url()).toContain('https://kotlinlang.org/multiplatform/'); + expect(page.url()).toContain('/multiplatform/'); const pageTitle = page.locator('h1').first(); await expect(pageTitle).toContainText('Kotlin Multiplatform'); }); diff --git a/test/production/solutions-tab.spec.ts b/test/production/solutions-tab.spec.ts index 07cb35b84dd..c4a73f23916 100644 --- a/test/production/solutions-tab.spec.ts +++ b/test/production/solutions-tab.spec.ts @@ -10,11 +10,11 @@ test.describe('Solutions tab', () => { await solutionsButton.click(); }); - test('Click on "Multiplatform" button should open the related page', async ({ page }) => { + test('Click on "Multiplatform" button should open the related page', async ({ page, baseURL }) => { const multiplatformButton = page.locator(testSelector("header")).getByText('Multiplatform').first(); await expect(multiplatformButton).toBeVisible(); await multiplatformButton.click(); - await expect(page.url()).toContain('https://kotlinlang.org/multiplatform/'); + expect(page.url()).toContain(`/multiplatform/`); }); test('Click on "Server-side" button should open the related page', async ({ page }) => { From 8d909e1b5d35ae649bf183ddace77ea2a2d6a2bc Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 15:21:57 +0100 Subject: [PATCH 17/24] fix(e2e): Replace absolute URLs with consistent relative path assertions - across `solutions-tab.spec.ts`, `grammar.spec.ts`, and `landings.spec.ts`. - Remove unused `baseURL` parameter from tests for improved clarity and maintenance. --- test/production/grammar.spec.ts | 2 +- test/production/landings.spec.ts | 8 ++++---- test/production/solutions-tab.spec.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/production/grammar.spec.ts b/test/production/grammar.spec.ts index 3e4c0506163..03b4b9969f4 100644 --- a/test/production/grammar.spec.ts +++ b/test/production/grammar.spec.ts @@ -32,7 +32,7 @@ test.describe('Grammar page', () => { await expect(grammar.getDeclarationById('kotlinFile')).toBeInViewport(); }); - test('Should redirect from .html to clean URL', async ({ page, baseURL }) => { + test('Should redirect from .html to clean URL', async ({ page }) => { await page.goto('/docs/reference/grammar.html'); // Should redirect to clean URL diff --git a/test/production/landings.spec.ts b/test/production/landings.spec.ts index 4009b38f756..2eb5eb873a8 100644 --- a/test/production/landings.spec.ts +++ b/test/production/landings.spec.ts @@ -6,8 +6,8 @@ test.describe.configure({ mode: 'parallel' }); test.describe('/lp/ pages list', async () => { test.skip(({ baseURL }) => !isProduction(baseURL), 'for host with reverse-proxy only'); - test(`Check /lp/multiplatform default redirects`, async ({ page, baseURL }) => { - const targetUrl = `/multiplatform/`; + test(`Check /lp/multiplatform default redirects`, async ({ page }) => { + const targetUrl = '/multiplatform/'; await page.goto('/lp/multiplatform'); expect(page.url()).toEqual(targetUrl); @@ -19,8 +19,8 @@ test.describe('/lp/ pages list', async () => { expect(page.url()).toEqual(targetUrl); }); - test(`Check /lp/multiplatform case-studies redirect`, async ({ page, baseURL }) => { - const targetUrl = `/case-studies/?type=multiplatform`; + test(`Check /lp/multiplatform case-studies redirect`, async ({ page }) => { + const targetUrl = '/case-studies/?type=multiplatform'; await page.goto('/lp/multiplatform/case-studies/'); expect(page.url()).toEqual(targetUrl); diff --git a/test/production/solutions-tab.spec.ts b/test/production/solutions-tab.spec.ts index c4a73f23916..f19ca6ac9f0 100644 --- a/test/production/solutions-tab.spec.ts +++ b/test/production/solutions-tab.spec.ts @@ -10,11 +10,11 @@ test.describe('Solutions tab', () => { await solutionsButton.click(); }); - test('Click on "Multiplatform" button should open the related page', async ({ page, baseURL }) => { + test('Click on "Multiplatform" button should open the related page', async ({ page }) => { const multiplatformButton = page.locator(testSelector("header")).getByText('Multiplatform').first(); await expect(multiplatformButton).toBeVisible(); await multiplatformButton.click(); - expect(page.url()).toContain(`/multiplatform/`); + expect(page.url()).toContain('/multiplatform/'); }); test('Click on "Server-side" button should open the related page', async ({ page }) => { From 219abf22c0b611ad4f7ef96a8d2363b1ab0a96a7 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 15:23:30 +0100 Subject: [PATCH 18/24] fix(test): update isDevelopment logic for localhost detection - Correct `isDevelopment` utility function to properly identify `localhost` as a development environment. --- test/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils.ts b/test/utils.ts index e6996bd056c..4355c3b1e6e 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -38,7 +38,7 @@ export function isProduction(baseURL: string | undefined) { export function isDevelopment(baseURL: string | undefined) { try { - return Boolean(baseURL) && new URL(baseURL).hostname !== 'localhost'; + return Boolean(baseURL) && new URL(baseURL).hostname === 'localhost'; } catch (error) { return false; } From 024dfd33b3676e6eeb58f1954e5363505cd1e950 Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 15:39:01 +0100 Subject: [PATCH 19/24] fix(test): update isDevelopment logic for localhost detection - Correct `isDevelopment` utility function to properly identify `localhost` as a development environment. --- test/production/landings.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/production/landings.spec.ts b/test/production/landings.spec.ts index 2eb5eb873a8..f18915b1c6c 100644 --- a/test/production/landings.spec.ts +++ b/test/production/landings.spec.ts @@ -6,8 +6,8 @@ test.describe.configure({ mode: 'parallel' }); test.describe('/lp/ pages list', async () => { test.skip(({ baseURL }) => !isProduction(baseURL), 'for host with reverse-proxy only'); - test(`Check /lp/multiplatform default redirects`, async ({ page }) => { - const targetUrl = '/multiplatform/'; + test(`Check /lp/multiplatform default redirects`, async ({ page, baseURL }) => { + const targetUrl = `${baseURL}/multiplatform/`; await page.goto('/lp/multiplatform'); expect(page.url()).toEqual(targetUrl); @@ -19,8 +19,8 @@ test.describe('/lp/ pages list', async () => { expect(page.url()).toEqual(targetUrl); }); - test(`Check /lp/multiplatform case-studies redirect`, async ({ page }) => { - const targetUrl = '/case-studies/?type=multiplatform'; + test(`Check /lp/multiplatform case-studies redirect`, async ({ page, baseURL }) => { + const targetUrl = `${baseURL}/case-studies/?type=multiplatform`; await page.goto('/lp/multiplatform/case-studies/'); expect(page.url()).toEqual(targetUrl); From ff5bcd7df042915f34674596a4f28fa608fa444c Mon Sep 17 00:00:00 2001 From: zoobestik Date: Tue, 27 Jan 2026 16:41:51 +0100 Subject: [PATCH 20/24] fix(e2e): update Playwright image to v1.57.0-noble --- docker-compose-e2e-statics.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-e2e-statics.yml b/docker-compose-e2e-statics.yml index e9cb0c22c70..eea31864af3 100644 --- a/docker-compose-e2e-statics.yml +++ b/docker-compose-e2e-statics.yml @@ -12,7 +12,7 @@ services: retries: 3 playwright: - image: mcr.microsoft.com/playwright:v1.57.0 + image: mcr.microsoft.com/playwright:v1.57.0-noble environment: BASE_URL: "http://nginx-server" WEBTEAM_UI_NPM_TOKEN: ${WEBTEAM_UI_NPM_TOKEN} From b5b658387f621f12e9e2e5d3e698670ce8af564c Mon Sep 17 00:00:00 2001 From: zoobestik Date: Mon, 2 Feb 2026 21:01:23 +0100 Subject: [PATCH 21/24] fix(e2e): remove`closeExternalBanners` utility and fix education pages - Add `data-test` attributes to various components in `education`, replacing CSS-based selectors for improved test reliability. - Remove `closeExternalBanners` utility and its usage across tests as it is no longer needed. - Update Playwright specs (`courses.spec.ts`, `education.spec.ts`) to use `data-test` attributes. - Refactor relative path navigation in tests for `CoursesPage` and `TeachPage` initialization. --- .../education/courses-list/courses-list.tsx | 6 +- .../education-layout/education-layout.tsx | 1 + .../subscription-form/subscription-form.tsx | 8 +- .../teach-launch-course.tsx | 2 +- .../education/teach-map/teach-map-marker.tsx | 2 +- .../education/teach-map/teach-map-tooltip.tsx | 2 +- blocks/education/teach-map/teach-map.tsx | 2 +- .../education/teach-numbers/teach-numbers.tsx | 4 +- pages/education/index.tsx | 42 ++++----- pages/education/why-teach-kotlin/index.tsx | 28 +++--- playwright.config.ts | 2 +- test/e2e/teach/courses.spec.ts | 23 ++--- test/e2e/teach/education.spec.ts | 42 ++++----- test/e2e/teach/utils.ts | 22 ++--- test/e2e/teach/why.spec.ts | 91 +++++++++---------- test/e2e/utils.ts | 10 +- test/page/teach/courses-page.ts | 2 +- test/page/teach/why-page.ts | 2 +- test/utils.ts | 8 -- 19 files changed, 142 insertions(+), 157 deletions(-) diff --git a/blocks/education/courses-list/courses-list.tsx b/blocks/education/courses-list/courses-list.tsx index db71c9b975d..0e39a3c59ce 100644 --- a/blocks/education/courses-list/courses-list.tsx +++ b/blocks/education/courses-list/courses-list.tsx @@ -22,14 +22,14 @@ interface CoursesListProps { export const CoursesList: FC = ({ universities }) => { const textCn = useTextStyles(); return ( -
-
+
+
University title
Location
Teaching Kotlin
{universities.map((university) => ( -
+
{university.title}
{university.location}
diff --git a/blocks/education/education-layout/education-layout.tsx b/blocks/education/education-layout/education-layout.tsx index b6db3893bfd..f2daad2827b 100644 --- a/blocks/education/education-layout/education-layout.tsx +++ b/blocks/education/education-layout/education-layout.tsx @@ -51,6 +51,7 @@ export const EducationLayout: FC = ({
{ }} > {({ setFieldValue }) => ( -
+ Subscribe form
@@ -66,12 +66,14 @@ export const SubscriptionForm: FC = () => { className={styles.input} /> - +
+ +
{isSubmitted ? ( Subscribe - + ) : ( diff --git a/blocks/education/teach-launch-course/teach-launch-course.tsx b/blocks/education/teach-launch-course/teach-launch-course.tsx index 6d2e0484a86..056b8e2d8cc 100644 --- a/blocks/education/teach-launch-course/teach-launch-course.tsx +++ b/blocks/education/teach-launch-course/teach-launch-course.tsx @@ -9,7 +9,7 @@ export const TeachLaunchCourse: FC = () => { return (
-
+
The Programming in Kotlin course is a comprehensive toolkit for teaching Kotlin and can be easily customized to align with specific educational needs. The course comes with slides, lecture notes, and assessment resources. diff --git a/blocks/education/teach-map/teach-map-marker.tsx b/blocks/education/teach-map/teach-map-marker.tsx index 43ffaea6281..a4003028116 100644 --- a/blocks/education/teach-map/teach-map-marker.tsx +++ b/blocks/education/teach-map/teach-map-marker.tsx @@ -14,7 +14,7 @@ interface TeachMapMarkerProps { export const TeachMapMarker: FC = ({ university, showTooltip, onClose }) => { return ( -
+
{showTooltip && }
); diff --git a/blocks/education/teach-map/teach-map-tooltip.tsx b/blocks/education/teach-map/teach-map-tooltip.tsx index 4f5399f3253..ee2bef1ba43 100644 --- a/blocks/education/teach-map/teach-map-tooltip.tsx +++ b/blocks/education/teach-map/teach-map-tooltip.tsx @@ -19,7 +19,7 @@ export const TeachMapTooltip: FC = ({ university, onClose ); return ( -
+
{university.title}
diff --git a/blocks/education/teach-map/teach-map.tsx b/blocks/education/teach-map/teach-map.tsx index 6f856d74d7a..48ce87ba1ce 100644 --- a/blocks/education/teach-map/teach-map.tsx +++ b/blocks/education/teach-map/teach-map.tsx @@ -32,7 +32,7 @@ export const TeachMap: FC = ({ className, universities }) => { return ( Map is unavailable
}> -
+
= ({ countriesCount, universiti
{countriesCount}
-
+
countries
@@ -21,7 +21,7 @@ export const TeachNumbers: FC = ({ countriesCount, universiti
{universitiesCount}
-
+
universities
diff --git a/pages/education/index.tsx b/pages/education/index.tsx index 4e6defab657..1631e082454 100644 --- a/pages/education/index.tsx +++ b/pages/education/index.tsx @@ -75,9 +75,9 @@ function EducationPage() {
-
+
-
+
-
+
-
+
-
+
-
-
+
+
Harvard University
-
+
University of Cambridge
-
+
Stanford University
-
+
Imperial College London
-
+
- + -
+

@@ -270,7 +270,7 @@ function EducationPage() {