diff --git a/test/e2e/api-references-template-customization.spec.ts b/test/e2e/api-references-template-customization.spec.ts
index 3d631d12c3b..fc8a639ea9e 100644
--- a/test/e2e/api-references-template-customization.spec.ts
+++ b/test/e2e/api-references-template-customization.spec.ts
@@ -1,22 +1,22 @@
-import { expect, test } from '@playwright/test';
+import { expect, Page, test } from '@playwright/test';
import { ApiReferencePage } from '../page/api-reference-page';
const pagesWithCustomizedTemplates = [
{
name: 'kotlinx.coroutines index',
- getInstance: (page) => new ApiReferencePage(page, '/api/kotlinx.coroutines/'),
+ getInstance: (page: Page) => new ApiReferencePage(page, '/api/kotlinx.coroutines/'),
},
{
name: 'kotlinx-coroutines-core module',
- getInstance: (page) => new ApiReferencePage(page, '/api/kotlinx.coroutines/kotlinx-coroutines-core/'),
+ getInstance: (page: Page) => new ApiReferencePage(page, '/api/kotlinx.coroutines/kotlinx-coroutines-core/'),
},
{
name: 'kotlinx-serialization index',
- getInstance: (page) => new ApiReferencePage(page, '/api/kotlinx.serialization/'),
+ getInstance: (page: Page) => new ApiReferencePage(page, '/api/kotlinx.serialization/'),
},
{
name: 'kotlinx-serialization-core module',
- getInstance: (page) => new ApiReferencePage(page, '/api/kotlinx.serialization/kotlinx-serialization-core/'),
+ getInstance: (page: Page) => new ApiReferencePage(page, '/api/kotlinx.serialization/kotlinx-serialization-core/'),
},
];
diff --git a/test/e2e/teach/courses.spec.ts b/test/e2e/teach/courses.spec.ts
new file mode 100644
index 00000000000..a3612181737
--- /dev/null
+++ b/test/e2e/teach/courses.spec.ts
@@ -0,0 +1,106 @@
+import { expect, test } from '@playwright/test';
+import { CoursesPage } from '../../page/teach/courses-page';
+import { closeExternalBanners } from '../utils';
+import { testSelector } from '../../utils';
+import { checkTeachCta, checkTeachMap, checkTeachNav } from './utils';
+
+test.describe('Courses page appearance and functionality', async () => {
+ test.beforeEach(async ({ page, context, baseURL }) => {
+ const coursesPage = new CoursesPage(page);
+ await coursesPage.init();
+ await closeExternalBanners(context, page, baseURL);
+ });
+
+ test('Should load the courses page correctly', async ({ page }) => {
+ // Check if the page title is correct
+ expect(await page.title()).toBe('List of Courses');
+
+ // Check if the main content is visible
+ const mainContent = page.locator(testSelector('teach-courses'));
+ await expect(mainContent).toBeVisible();
+
+ // Check if the page heading is correct
+ const title = mainContent.locator('h1');
+ await expect(title).toBeVisible();
+
+ expect(await title.textContent()).toBe('Universities That Teach Kotlin');
+ });
+
+ test('Should have working navigation buttons', async ({ page }) => {
+ await checkTeachNav(page, 'List of Courses');
+ });
+
+ test('Should have working tab navigation', async ({ page }) => {
+ // Check if the tab list is visible
+ const tabList = page.locator(testSelector('tab-list'));
+ await expect(tabList).toBeVisible();
+
+ // Check if both tabs are present
+ const tableViewTab = tabList.getByRole('tab', { name: 'Table view', exact: true });
+ await expect(tableViewTab).toBeVisible();
+
+ const mapViewTab = tabList.getByRole('tab', { name: 'Map view', exact: true });
+ await expect(mapViewTab).toBeVisible();
+
+ await expect(tableViewTab).toHaveAttribute('data-test', 'tab tab-selected');
+ await expect(mapViewTab).toHaveAttribute('data-test', 'tab');
+
+ // CoursesList should be visible in the table view
+ await expect(page.locator('.ktl-courses-list')).toBeVisible();
+
+ // Switch to the map view
+ await mapViewTab.click();
+
+ // Map view should now be active
+ await expect(tableViewTab).toHaveAttribute('data-test', 'tab');
+ await expect(mapViewTab).toHaveAttribute('data-test', 'tab tab-selected');
+
+ // TeachMap should be visible in the map view
+ await expect(page.locator('.teach-map .gm-style')).toBeVisible();
+
+ // Switch back to the table view
+ await tableViewTab.click();
+
+ // Table view should be active again
+ await expect(tableViewTab).toHaveAttribute('data-test', 'tab tab-selected');
+ await expect(mapViewTab).toHaveAttribute('data-test', 'tab');
+
+ // CoursesList should be visible again
+ await expect(page.locator('.ktl-courses-list')).toBeVisible();
+ });
+
+ test('Should display university list in table view', async ({ page }) => {
+ // Make sure we're in a table view
+ const tableViewTab = page.getByRole('tab', { name: 'Table view' });
+ await tableViewTab.click();
+
+ // Check if the course list is visible
+ const coursesList = page.locator('.ktl-courses-list');
+ await expect(coursesList).toBeVisible();
+
+ // Check and verify the headers of the course list
+ const headers = coursesList.locator('.ktl-courses-list-header .ktl-courses-list-cell');
+ expect(await headers.count()).toBe(3);
+ expect(await headers.nth(0).textContent()).toBe('University title');
+ expect(await headers.nth(1).textContent()).toBe('Location');
+ expect(await headers.nth(2).textContent()).toBe('Teaching Kotlin');
+
+ // Check if there are universities in the list
+ const universities = coursesList.locator('.ktl-courses-list__item');
+ expect(await universities.count()).toBeGreaterThan(0);
+ expect(await universities.first().locator('.ktl-courses-list-cell').count())
+ .toBe(3);
+ });
+
+ test('Should display map with markers in map view', async ({ page }) => {
+ // Switch to the map view
+ const mapViewTab = page.getByRole('tab', { name: 'Map view' });
+ await mapViewTab.click();
+
+ // Check if the map is visible
+ const map = page.locator('.teach-map');
+ await checkTeachMap(page, map);
+ });
+
+ test('Should have action buttons for educators', checkTeachCta);
+});
\ No newline at end of file
diff --git a/test/e2e/teach/education.spec.ts b/test/e2e/teach/education.spec.ts
new file mode 100644
index 00000000000..fa1fe172bae
--- /dev/null
+++ b/test/e2e/teach/education.spec.ts
@@ -0,0 +1,254 @@
+import { expect, test } from '@playwright/test';
+import { TeachPage } from '../../page/teach/education';
+import { closeExternalBanners } from '../utils';
+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 }) => {
+ const teachPage = new TeachPage(page);
+ await teachPage.init();
+ await closeExternalBanners(context, page, baseURL);
+ });
+
+ test('Should load the education page correctly', async ({ page }) => {
+ // Check if the page title is correct
+ expect(await page.title()).toBe('Kotlin for Education');
+
+ // Check if the main content is visible
+ const mainContent = page.locator(testSelector('teach-index-page'));
+ await expect(mainContent).toBeVisible();
+
+ // Check if the page heading is correct
+ const title = mainContent.locator('h1');
+ await expect(title).toBeVisible();
+
+ expect(await title.textContent()).toBe('Teach Computer Science with Kotlin');
+ });
+
+ test('Should have working navigation buttons', async ({ page }) => {
+ await checkTeachNav(page, 'Overview');
+ });
+
+ test('Should display course materials download button', async ({ page }) => {
+ const block = page.locator('.teach-launch-course__text');
+
+ // Locate and verify the download button with the exact name match
+ const button = block.getByRole('link', { name: 'Download all materials →', exact: true });
+ await expect(button).toBeVisible();
+ await expect(button).toHaveAttribute('href', MATERIALS_LINK);
+
+ expect(await block.screenshot()).toMatchSnapshot('launch-course-text.png');
+ });
+
+ test('Should display features section with features', async ({ page }) => {
+ // Check if the features section is visible
+ const featuresSection = page.locator('.teach-features');
+ await expect(featuresSection).toBeVisible();
+
+ // Check if there are exactly 3 features
+ const expectedFeatures = ['Academically recognized', 'Language of the industry', 'Multiplatform'];
+ const features = featuresSection.locator('.teach-feature');
+ expect(await features.count()).toBe(3);
+
+ // Check each feature
+ for (let i = 0; i < expectedFeatures.length; i++) {
+ const feature = features.nth(i);
+ await expect(feature.locator('.teach-feature__icon img')).toBeVisible();
+ expect(await feature.locator('.ktl-h3').textContent()).toBe(expectedFeatures[i]);
+ }
+
+ expect(await featuresSection.screenshot()).toMatchSnapshot('teach-features.png');
+ });
+
+ test('Should display buttons in top section', async ({ page }) => {
+ // Get the top buttons block container
+ const block = page.locator('.teach-top-buttons');
+
+ // Check the "Join Educators Community" button visibility and link
+ const join = block.getByRole('link', { name: 'Join Educators Community', exact: true });
+ await expect(join).toBeVisible();
+ await expect(join).toHaveAttribute('href', SIGNUP_LINK);
+
+ // Check the "Why Teach Kotlin" button visibility and link
+ const why = block.getByRole('link', { name: 'Why Teach Kotlin →', exact: true });
+ await expect(why).toBeVisible();
+ await expect(why).toHaveAttribute('href', 'why-teach-kotlin.html');
+
+ expect(await block.screenshot()).toMatchSnapshot('teach-top-mobile-buttons.png');
+ });
+
+ test('Should display university statistics correctly', async ({ page }) => {
+ // Check if the university count is displayed
+ const universitiesText = page.locator('.universities-top__title h2');
+ expect(await universitiesText.textContent()).toBe('Kotlin Courses Around the World');
+
+ // Check if the TeachNumbers component is visible
+ const teachNumbers = page.locator('.universities-top__numbers');
+ await expect(teachNumbers).toBeVisible();
+
+ // Check if the TeachNumbers component is visible
+ const subtitles = teachNumbers.locator('.teach-number__subtitle');
+ expect(await subtitles.count()).toBe(2);
+ expect(await subtitles.nth(0).textContent()).toBe('countries');
+ expect(await subtitles.nth(1).textContent()).toBe('universities');
+ });
+
+ test('Should display university logos', async ({ page }) => {
+ // Check if the university logos are visible
+ const logos = page.locator('.teach-logos__logo');
+ expect(await logos.count()).toBe(5);
+
+ // Check specific universities
+ await expect(page.locator('img[alt="Harvard University"]')).toBeVisible();
+ await expect(page.locator('img[alt="University of Cambridge"]')).toBeVisible();
+ await expect(page.locator('img[alt="Stanford University"]')).toBeVisible();
+ 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');
+ });
+
+ test('Should have a working interactive map', async ({ page }) => {
+ // Check if the map is visible
+ const map = page.locator('.teach-map__wrapper');
+ await checkTeachMap(page, map);
+ });
+
+ test('Should display navigation buttons', async ({ page }) => {
+ const bottom = page.locator('.teach-universities__bottom');
+ await expect(bottom).toBeVisible();
+
+ // Check if the mailto button is visible and working
+ const mailtoButton = bottom.getByRole('link', { name: 'education@kotlinlang.org.', exact: true });
+ await expect(mailtoButton).toBeVisible();
+ await expect(mailtoButton).toHaveAttribute('href', MAILTO_LINK);
+
+ // Check if the "All Universities" button is visible
+ const allUniversitiesButton = bottom.getByRole('link', { name: 'All universities', exact: true });
+ await expect(allUniversitiesButton).toBeVisible();
+ await expect(allUniversitiesButton).toHaveAttribute('href', 'courses.html');
+ });
+
+ test('Should have comprehensive resource links section', async ({ page }) => {
+ // Check if the resources section is visible
+ const resourcesSection = page.locator('#start-teaching-kotlin');
+ await expect(resourcesSection).toBeVisible();
+
+ // Check section title
+ const sectionTitle = resourcesSection.locator('h2');
+ await expect(sectionTitle).toHaveText('Start Teaching Kotlin with These Resources');
+
+ // Check category headings
+ const expectedTitles = ['Get started', 'Tools', 'Online Courses', 'Android in Kotlin', 'Practice Kotlin'];
+ const categoryHeadings = resourcesSection.locator('.ktl-h4');
+ await expect(categoryHeadings).toHaveCount(expectedTitles.length);
+
+ // Check each category heading and its associated links
+ for (let i = 0; i < expectedTitles.length; i++) {
+ const item = categoryHeadings.nth(i);
+ await expect(item, `Category heading should be ${expectedTitles[i]}`).toHaveText(expectedTitles[i]);
+ const links = item.locator(':scope + .teach-list > li');
+ expect(await links.count(), `${expectedTitles[i]} category should have at least one link`).toBeGreaterThan(0);
+ }
+ });
+
+ test('Should have a working subscription form', async ({ page }) => {
+ const email = 'test@example.com';
+
+ await page.route('https://forms-service.jetbrains.com/marketo', route => {
+ if (route.request().method() !== 'POST') route.continue();
+ route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ 'success': true, 'cause': [] })
+ });
+ });
+
+ await page.route(`https://account.jetbrains.com/services/acceptAgreement?email=${encodeURIComponent(email)}&type=mkt.newsletter.visitor`, route => {
+ if (route.request().method() !== 'POST') route.continue();
+ route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ 'status': 'OK' })
+ });
+ });
+
+ // Locate the subscription form
+ const subscriptionForm = page.locator('.teach-subscription-section');
+ await expect(subscriptionForm).toBeVisible();
+
+ // Find the form elements
+ await subscriptionForm.locator('input[name="Email"]').fill(email);
+
+ // Check the privacy checkbox by clicking its label
+ await subscriptionForm.locator('.teach-subscription-form__checkbox label').click();
+
+ // Submit the form
+ await subscriptionForm.locator('button[type="submit"]').click();
+
+ // 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');
+ });
+
+ test('Should have a working YouTube player', async ({ page }) => {
+ // Check if the YouTube player container is visible
+ const youtubePlayer = page.locator('.teach-video');
+ await expect(youtubePlayer).toBeVisible();
+
+ // Check if the player is in "show video" mode
+ await expect(youtubePlayer.locator('[class*="ktl-youtube-player-module_preview_"]')).toBeVisible();
+
+ // Check if the play button is visible
+ const playButton = youtubePlayer.locator('[class*="ktl-youtube-player-module_play-button_"]');
+ await expect(playButton).toBeVisible();
+
+ // Click the play button to start the video
+ await playButton.click();
+
+ // After clicking, the play button should be hidden and the video should be playing
+ await expect(playButton).toBeHidden();
+
+ // Check if the iframe is loaded correctly
+ const iframe = youtubePlayer.locator('iframe');
+ await expect(iframe).toBeVisible();
+
+ // Check if the iframe has the correct src attribute (YouTube embed URL)
+ const iframeSrc = await iframe.getAttribute('src');
+ expect(iframeSrc).toBeTruthy();
+ expect(iframeSrc).toContain('youtube.com');
+ expect(iframeSrc).toContain('PLlFc5cFwUnmzT4cgLOGJYGnY6j0W2xoFA');
+ });
+
+ test('Should have working quotes slider', async ({ page }) => {
+ const quotes = page.locator('[class*=ktl-quotes-slider-module_quotes-slider_]');
+
+ // Initial state of quotes
+ const defaultAuthor = quotes.getByText('David Vaughn', { exact: false });
+ await expect(defaultAuthor).toBeVisible();
+
+ const quoteContent = quotes.getByText('Kotlin is faster to develop', { exact: false });
+ await expect(quoteContent).toBeVisible();
+
+ // Controls of navigation
+ const controls = page.locator('[class*=ktl-quotes-slider-module_control_]');
+ await expect(controls).toHaveCount(2);
+
+ const backButton = controls.first();
+ await expect(backButton).not.toHaveClass(/ktl-quotes-slider-module_control-active_/);
+
+ const forwardButton = controls.last();
+ await expect(forwardButton).toHaveClass(/ktl-quotes-slider-module_control-active_/);
+
+ await forwardButton.click();
+
+ await expect(quotes.getByText('Sergey Bratus')).toBeVisible();
+ await expect(quotes.getByText('I used Kotlin')).toBeVisible();
+
+ await expect(backButton).toHaveClass(/ktl-quotes-slider-module_control-active_/);
+ });
+
+ test('Should have action buttons for educators', checkTeachCta);
+});
\ No newline at end of file
diff --git a/test/e2e/teach/utils.ts b/test/e2e/teach/utils.ts
new file mode 100644
index 00000000000..8cb503f479a
--- /dev/null
+++ b/test/e2e/teach/utils.ts
@@ -0,0 +1,102 @@
+import { expect, Locator, Page } from '@playwright/test';
+
+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;
+export const MATERIALS_LINK = 'https://drive.google.com/drive/folders/1nN3LuyEfmBaSDZpnb4VA9kDuLakmVXH1?usp=drive_link' as const;
+
+const NAV_LINKS = [
+ ['Overview', '/education/'],
+ ['Why Teach Kotlin', '/education/why-teach-kotlin.html'],
+ ['List of Courses', '/education/courses.html']
+] as const;
+
+export async function checkTeachNav(page: Page, selected: typeof NAV_LINKS[number][0]) {
+ const navBar = page.locator('.teach-sticky-menu');
+
+ // Check section title to root of nav
+ const sectionLink = navBar.getByRole('link', { name: 'Teach', exact: true });
+ await expect(sectionLink).toBeVisible();
+ await expect(sectionLink).toHaveAttribute('href', '/education/');
+
+ // Check the Join button
+ const joinLink = navBar.getByRole('link', { name: 'Join Educators', exact: true });
+ await expect(joinLink).toBeVisible();
+ await expect(joinLink).toHaveAttribute('href', SIGNUP_LINK);
+
+ const subNav = navBar.getByRole('navigation');
+
+ // Check sub-nav link
+ for (let [title, link] of NAV_LINKS) {
+ if (title == selected) {
+ await expect(subNav.getByRole('link', { name: title })).toHaveCount(0);
+ await expect(subNav.getByText(title)).toBeVisible();
+ continue;
+ }
+
+ const subLink = subNav.getByRole('link', { name: title }).filter({ visible: true });
+ await expect(subLink).toHaveAttribute('href', link);
+ }
+
+ expect(await navBar.screenshot()).toMatchSnapshot('sticky-menu.png');
+}
+
+export async function checkTeachMap(page: Page, map: Locator) {
+ await expect(map).toBeVisible();
+ await expect(map.locator('.gm-style')).toBeVisible();
+
+ // Wait for the Google Maps API to attach itself
+ await page.waitForFunction(() => window['google'] && window['google']['maps']);
+
+ // The map should have markers
+ const markers = page.locator('.teach-map-marker');
+ expect(await markers.count()).toBeGreaterThan(100);
+
+ // Wait for the Google Maps Markers to attach
+ await page.waitForFunction(() => {
+ const markers = document.querySelectorAll('.teach-map-marker');
+ return markers.length > 100 &&
+ Array.from(markers).every(marker =>
+ marker['offsetHeight'] > 0 && marker['offsetWidth'] > 0
+ );
+ });
+
+ // Check if at least one marker is visible
+ let marker = markers.last();
+ await expect(marker).toBeVisible();
+ await expect(marker).not.toHaveClass('teach-map-marker teach-map-marker_active');
+
+ await marker.scrollIntoViewIfNeeded();
+
+ // !!!ATTENTION!!!: marker.click and marker.hover don't work for Google Maps.
+ const box = await marker.boundingBox();
+ await page.mouse.move(box.x, box.y);
+ await page.mouse.down();
+ await page.waitForTimeout(100);
+ await page.mouse.up();
+
+ await expect(marker).toHaveClass('teach-map-marker teach-map-marker_active');
+
+ const tooltip = marker.locator('.teach-map-tooltip');
+ await expect(tooltip).toBeVisible();
+}
+
+export async function checkTeachCta({ page }) {
+ // Get the CTA wrapper element from the page
+ const connectUs = page.locator('section [class*=ktl-cta-block-module_wrapper_]');
+
+ // Check if the "Connect with us" heading is visible
+ const title = connectUs.getByRole('heading', { name: 'Connect with us', exact: true });
+ await expect(title).toBeVisible();
+
+ // Check if the Slack link is visible and has the correct href
+ const slackLink = connectUs.getByRole('link', { name: 'Slack-channel →', exact: true });
+ await expect(slackLink).toBeVisible();
+ await expect(slackLink).toHaveAttribute('href', SIGNUP_LINK);
+
+ // Check if the email link is visible and has the correct mailto href
+ const eduLink = connectUs.getByRole('link', { name: 'education@kotlinlang.org', exact: true });
+ await expect(eduLink).toBeVisible();
+ await expect(eduLink).toHaveAttribute('href', MAILTO_LINK);
+
+ expect(await connectUs.screenshot()).toMatchSnapshot('connect-us.png');
+}
diff --git a/test/e2e/teach/why.spec.ts b/test/e2e/teach/why.spec.ts
new file mode 100644
index 00000000000..9866b8a5095
--- /dev/null
+++ b/test/e2e/teach/why.spec.ts
@@ -0,0 +1,244 @@
+import { expect, test } from '@playwright/test';
+import { WhyTeachPage } from '../../page/teach/why-page';
+import { closeExternalBanners } from '../utils';
+import { checkTeachCta, checkTeachNav } from './utils';
+import { testSelector } from '../../utils';
+
+const LIST_OF_SECTION = [
+ ['Academically recognized', 2, 'Eugeniy Tyumentcev, Omsk State University'],
+ ['Language of the industry', 2, 'Ryan Stansifer, Florida Institute of Technology'],
+ ['Multiplatform', 1, 'Jakob Mass, University of Tartu'],
+ ['Interoperable', 2, 'David Vaughn, University of Missouri–St. Louis'],
+ ['Supports multiple paradigms', 3, 'Alexey Mitsyuk, HSE university'],
+ ['Modern, concise, and safe', 4, 'Nick Efford, University of Leeds'],
+ ['Tooling', 2, 'Mariusz Nowostawski, Norwegian University of Science and Technology']
+] as const;
+
+function toId(label: typeof LIST_OF_SECTION[number][0]) {
+ return label.toLowerCase()
+ .replace(/ /g, '-')
+ .replace(/[^a-z-]/g, '');
+}
+
+test.describe('Why Teach Kotlin page appearance and functionality', async () => {
+ test.beforeEach(async ({ page, context, baseURL }) => {
+ const whyTeachPage = new WhyTeachPage(page);
+ await whyTeachPage.init();
+ await closeExternalBanners(context, page, baseURL);
+ });
+
+ test('Should load the Why Teach Kotlin page correctly', async ({ page }) => {
+ // Check if the page title is correct
+ expect(await page.title()).toBe('Why teach Kotlin');
+
+ // Check if the main content is visible
+ const mainContent = page.locator(testSelector('teach-why-teach-page'));
+ await expect(mainContent).toBeVisible();
+
+ // Check if the page heading is correct
+ const heading = page.getByRole('heading', { name: 'Why Teach Kotlin', exact: true });
+ await expect(heading).toBeVisible();
+ });
+
+ test('Should have working navigation buttons', async ({ page }) => {
+ await checkTeachNav(page, 'Why Teach Kotlin');
+ });
+
+ test('Should display all sections with quotes correctly', async ({ page }) => {
+ // Check if the navigation menu is visible
+ const content = page.locator('.why-teach-grid__content');
+ await expect(content).toBeVisible();
+
+ // Check if all navigation items are present
+ const sections = content.locator(':scope > section');
+ expect(await sections.count()).toBe(7);
+
+ // Check specific navigation items
+ for (const [label, quotesSize, expectedAuthor] of LIST_OF_SECTION) {
+ const section = sections.locator(`:scope#${toId(label)}`);
+ await expect(section, `Section "${label}" should be visible`).toBeVisible();
+
+ const title = section.locator(`:scope > h3`);
+ await expect(title, `Title for section "${label}" should be visible`).toBeVisible();
+ expect(await title.textContent(), `Title text for section "${label}" should match`).toBe(label);
+
+ // Check if the quote slider exists
+ const quotesSlider = section.locator('[class*=ktl-quotes-slider-module_quotes-slider_]');
+ await expect(quotesSlider, `Quote slider for section "${label}" should be visible`).toBeVisible();
+
+ // Check if the quote text is visible (using first() to handle multiple elements)
+ const quoteText = quotesSlider.locator('[class*=ktl-quotes-slider-module_quote-text_]').first();
+ await expect(quoteText, `Quote text for section "${label}" should be visible`).toBeVisible();
+ expect(await quoteText.textContent(), `Quote text for section "${label}" should not be empty`).not.toBe('');
+
+ // Check if the author is visible (using a more specific selector)
+ const author = quotesSlider.locator('[class*=ktl-quotes-slider-module_nav_] > div:first-child');
+ await expect(author, `Author for section "${label}" should be visible`).toBeVisible();
+ expect(await author.textContent(), `Author for section "${label}" should match expected`).toBe(expectedAuthor);
+
+ if (quotesSize < 2) {
+ const controls = quotesSlider.locator('[class*=ktl-quotes-slider-module_control_]');
+ expect(await controls.count(), `Controls for section "${label}" should not exist`).toBe(0);
+ return;
+ }
+
+ // Check for navigation controls - but don't fail if they don't exist
+ const controls = quotesSlider.locator('[class*=ktl-quotes-slider-module_control_]');
+ expect(await controls.count(), `Navigation controls count for section "${label}" should be 2`).toBe(2);
+
+ // Check if the slide count is visible
+ const slidesCount = quotesSlider.locator('[class*=ktl-quotes-slider-module_slides-count_]');
+ expect(await slidesCount.count(), `Slides count element for section "${label}" should exist`).toBe(1);
+ await expect(slidesCount, `Slides count for section "${label}" should be visible`).toBeVisible();
+
+ expect(await slidesCount.textContent(), `For section "${label}" slides should be ${quotesSize}`).toBe(`1/${quotesSize}`);
+ }
+ });
+
+ test('Check the "Academically recognized" section', async ({ page }) => {
+ await expect(page.locator('#academically-recognized .quote-section__content')).toContainText('We know of over 300 universities');
+
+ const academicallyRecognizedInfo = page.locator('#academically-recognized .quote-section__info');
+
+ const statNumber = academicallyRecognizedInfo.locator('.ktl-hero');
+ await expect(statNumber).toBeVisible();
+ expect(await statNumber.textContent()).toBe('32');
+
+ const description = academicallyRecognizedInfo.locator('.ktl-dimmed-text');
+ await expect(description).toBeVisible();
+ expect(await description.textContent()).toContain('top 100 universities');
+
+ const link = academicallyRecognizedInfo.locator('a[href="courses.html"]');
+ await expect(link).toBeVisible();
+ expect(await link.textContent()).toBe('List of universities ↗');
+ });
+
+ test('Check the "Language of the industry" section', async ({ page }) => {
+ // Check list items in the "Language of the industry" section
+ const industrySection = page.locator('#language-of-the-industry');
+ const listItems = industrySection.locator('.quote-section__content ul.rs-ul > li');
+
+ // Verify there are multiple list items
+ expect(await listItems.count()).toBe(4);
+
+ // Check the content of specific list items
+ await expect(listItems.nth(0)).toContainText('Kotlin is used by top companies');
+ await expect(listItems.nth(1)).toContainText('Teaching professional software engineering practices');
+ await expect(listItems.nth(2)).toContainText('One out of every two developers');
+ await expect(listItems.nth(3)).toContainText('In 2020, Kotlin became the 2nd most popular language');
+
+ const industrySectionInfo = industrySection.locator('.quote-section__info');
+
+ const description = industrySectionInfo.locator('.ktl-dimmed-text');
+ await expect(description).toBeVisible();
+ await expect(description).toContainText('Kotlin has consistently ranked');
+
+ const link = industrySectionInfo.locator('a[href="https://hired.com/state-of-software-engineers/2023/" ]');
+ await expect(link).toBeVisible();
+ expect(await link.textContent()).toBe('Hired’s 2023 State of Software Engineers ↗');
+ });
+
+ test('Check the "Multiplatform" section', async ({ page }) => {
+ const multiplatformSection = page.locator('#multiplatform');
+ await expect(multiplatformSection.locator('.quote-section__content')).toContainText('Kotlin is a top choice for teaching Android development');
+
+ const multiplatformSectionInfo = multiplatformSection.locator('.quote-section__info');
+
+ const description = multiplatformSectionInfo.locator('.ktl-dimmed-text');
+ await expect(description).toBeVisible();
+ await expect(description).toContainText('Compose Multiplatform frameworks from JetBrains');
+
+ const link = multiplatformSectionInfo.locator('a[href="https://developers.googleblog.com/2023/05/bringing-kotlin-to-web.html"]');
+ await expect(link).toBeVisible();
+ expect(await link.textContent()).toBe('Google for Developers blog, 2023 ↗');
+ });
+
+ test('Check the "Interoperable" section', async ({ page }) => {
+ const interoperableSection = page.locator('#interoperable');
+ await expect(interoperableSection.locator('.quote-section__content')).toContainText('Seamless interoperability with the JVM ecosystem');
+
+ const interoperableSectionInfo = interoperableSection.locator('.quote-section__info');
+
+ const description = interoperableSectionInfo.locator('.ktl-dimmed-text');
+ await expect(description).toBeVisible();
+ await expect(description).toContainText('Kotlin can also be compiled into');
+
+ const link1 = interoperableSectionInfo.locator('a[href="https://kotlinlang.org/docs/mixing-java-kotlin-intellij.html#converting-an-existing-java-file-to-kotlin-with-j2k"]');
+ await expect(link1).toBeVisible();
+ expect(await link1.textContent()).toBe('Java-to-Kotlin converter ↗');
+
+ const link2 = interoperableSectionInfo.locator('a[href="https://kotlinlang.org/docs/jvm-get-started.html"]');
+ await expect(link2).toBeVisible();
+ expect(await link2.textContent()).toBe('Kotlin/JVM ↗');
+
+ const link3 = interoperableSectionInfo.locator('a[href="https://kotlinlang.org/docs/native-get-started.html"]');
+ await expect(link3).toBeVisible();
+ expect(await link3.textContent()).toBe('Kotlin/Native ↗');
+ });
+
+ test('Check the "Supports multiple paradigms" section', async ({ page }) => {
+ const paradigmsSection = page.locator('#supports-multiple-paradigms');
+ await expect(paradigmsSection.locator('.quote-section__content')).toContainText('Kotlin combines all the major programming paradigms');
+
+ const paradigmsSectionInfo = paradigmsSection.locator('.quote-section__info');
+
+ const description = paradigmsSectionInfo.locator('.ktl-dimmed-text');
+ await expect(description).toBeVisible();
+ await expect(description).toContainText('Kotlin supports functional, imperative');
+
+ const link = paradigmsSectionInfo.locator('a[href]');
+ await expect(link).not.toBeVisible();
+ });
+
+ test('Check the "Modern, concise, and safe" section', async ({ page }) => {
+ const modernSection = page.locator('#modern-concise-and-safe');
+ await expect(modernSection.locator('.quote-section__content')).toContainText('Kotlin allows students to focus on expressing their ideas');
+
+ const modernSectionInfo = modernSection.locator('.quote-section__info');
+
+ const description = modernSectionInfo.locator('.ktl-dimmed-text');
+ expect(await description.count()).toBe(2);
+
+ await expect(description.nth(0)).toBeVisible();
+ await expect(description.nth(0)).toContainText('Type safety, null safety, and expressive');
+
+ await expect(description.nth(1)).toBeVisible();
+ await expect(description.nth(1)).toContainText('Source: an internal study on teaching Kotlin');
+
+ const link = modernSectionInfo.locator('a[href]');
+ await expect(link).not.toBeVisible();
+ });
+
+ test('Check the "Tooling" section', async ({ page }) => {
+ // Check content in the "Tooling" section
+ const toolingSection = page.locator('#tooling');
+ await expect(toolingSection.locator('.quote-section__content')).toContainText('Many of the top professional tools are packaged with the language');
+
+ const toolingSectionInfo = toolingSection.locator('.quote-section__info');
+
+ 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"]');
+ await expect(link1).toBeVisible();
+ expect(await link1.textContent()).toBe('Free IntelliJ IDEA Ultimate license ↗');
+
+ 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"]');
+ 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/"]');
+ 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&"]');
+ await expect(link5).toBeVisible();
+ expect(await link5.textContent()).toBe('Kotlin tracks by JetBrains Academy ↗');
+ });
+
+ test('Should have action buttons for educators', checkTeachCta);
+});
diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts
index 8ba80916a5a..e9c7c19c534 100644
--- a/test/e2e/utils.ts
+++ b/test/e2e/utils.ts
@@ -1,4 +1,5 @@
-import { Page, ElementHandle } from '@playwright/test';
+import { BrowserContext, ElementHandle, Page } from '@playwright/test';
+import { closeCookiesConsentBanner } from '../utils';
export async function getElementScreenshotWithPadding(page: Page, element: ElementHandle, padding: number): Promise {
await element.scrollIntoViewIfNeeded();
@@ -15,3 +16,13 @@ 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();
+ }
+}
diff --git a/test/e2e/webhelp.spec.ts b/test/e2e/webhelp.spec.ts
index 7f59887e24f..3919d2b1e27 100644
--- a/test/e2e/webhelp.spec.ts
+++ b/test/e2e/webhelp.spec.ts
@@ -10,8 +10,6 @@ import {
import { getElementScreenshotWithPadding } from './utils';
import os from 'os';
-const MAX_DIFF_PIXEL_RATIO = 0.011;
-
test.describe('WebHelp page appearance', async () => {
test.beforeEach(async ({ page }) => {
const webHelpPage = new WebHelpPage(page, '/docs/test-page.html');
@@ -108,10 +106,7 @@ test.describe('WebHelp page appearance', async () => {
test(`Should render layout of the article properly on ${resolutionName}`, async ({ page }) => {
await page.setViewportSize(resolution);
const screenshot = await page.screenshot({ fullPage: true });
- expect(screenshot).toMatchSnapshot({
- name: `layout_${resolutionName}.png`,
- maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO
- });
+ expect(screenshot).toMatchSnapshot(`layout_${resolutionName}.png`);
});
test(`Should render micro format properly on ${resolutionName}`, async ({ page }) => {
@@ -168,10 +163,7 @@ test.describe('WebHelp page appearance', async () => {
await page.setViewportSize(resolution);
const codeBlock = page.locator(testSelector('code-block')).filter({ hasText: 'MessageService' }).first();
const screenshot = await codeBlock.screenshot();
- expect(screenshot).toMatchSnapshot({
- name: `code-block_${resolutionName}.png`,
- maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO
- });
+ expect(screenshot).toMatchSnapshot(`code-block_${resolutionName}.png`);
});
test(`Should render hovered codeblock properly on ${resolutionName}`, async ({ page }) => {
@@ -179,10 +171,7 @@ test.describe('WebHelp page appearance', async () => {
const codeBlock = page.locator(testSelector('code-block')).filter({ hasText: 'MessageService' }).first();
await codeBlock.hover();
const screenshot = await codeBlock.screenshot();
- expect(screenshot).toMatchSnapshot({
- name: `code-block_hovered_${resolutionName}.png`,
- maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO
- });
+ expect(screenshot).toMatchSnapshot(`code-block_hovered_${resolutionName}.png`);
});
test(`Should render expandable codeblock properly on ${resolutionName}`, async ({ page }) => {
@@ -190,10 +179,7 @@ test.describe('WebHelp page appearance', async () => {
const codeBlock = page.locator(testSelector('code-collapse')).filter({ hasText: 'package' }).first();
const codeBlockElement = await codeBlock.elementHandle();
const screenshot = await getElementScreenshotWithPadding(page, codeBlockElement, ELEMENT_PADDING_OFFSET);
- expect(screenshot).toMatchSnapshot({
- name: `code-block_expandable_${resolutionName}.png`,
- maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO
- });
+ expect(screenshot).toMatchSnapshot(`code-block_expandable_${resolutionName}.png`);
});
test(`Should render expandable codeblock when expanded properly on ${resolutionName}`, async ({ page }) => {
@@ -203,10 +189,7 @@ test.describe('WebHelp page appearance', async () => {
await page.waitForTimeout(MICRO_ANIMATION_TIMEOUT_LONG);
const codeBlockElement = await codeBlock.elementHandle();
const screenshot = await getElementScreenshotWithPadding(page, codeBlockElement, ELEMENT_PADDING_OFFSET);
- expect(screenshot).toMatchSnapshot({
- name: `code-block_expandable_expanded_${resolutionName}.png`,
- maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO
- });
+ expect(screenshot).toMatchSnapshot(`code-block_expandable_expanded_${resolutionName}.png`);
});
test(`Should render collapsed codeblock properly on ${resolutionName}`, async ({ page }) => {
@@ -219,10 +202,7 @@ test.describe('WebHelp page appearance', async () => {
await page.waitForTimeout(MICRO_ANIMATION_TIMEOUT_LONG);
const codeBlockElement = await codeBlock.elementHandle();
const screenshot = await getElementScreenshotWithPadding(page, codeBlockElement, ELEMENT_PADDING_OFFSET);
- expect(screenshot).toMatchSnapshot({
- name: `code-block_expandable_${resolutionName}.png`,
- maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO
- });
+ expect(screenshot).toMatchSnapshot(`code-block_expandable_${resolutionName}.png`);
});
test(`Should render playground properly on ${resolutionName}`, async ({ page }) => {
@@ -277,10 +257,7 @@ test.describe('WebHelp page appearance', async () => {
await page.setViewportSize(resolution);
const element = page.locator('div.table').filter({ hasText: 'Dependencies' }).first();
const screenshot = await element.screenshot();
- expect(screenshot).toMatchSnapshot({
- name: `table_complex_codeblocks_${resolutionName}.png`,
- maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO
- });
+ expect(screenshot).toMatchSnapshot(`table_complex_codeblocks_${resolutionName}.png`);
});
test(`Should render ordered list properly on ${resolutionName}`, async ({ page }) => {
@@ -345,10 +322,7 @@ test.describe('WebHelp page appearance', async () => {
await page.locator('dt').first().click();
await page.waitForTimeout(MICRO_ANIMATION_TIMEOUT);
const screenshot = await getElementScreenshotWithPadding(page, element, ELEMENT_PADDING_OFFSET);
- expect(screenshot).toMatchSnapshot({
- name: `definition-list_expanded_${resolutionName}.png`,
- maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO
- });
+ expect(screenshot).toMatchSnapshot(`definition-list_expanded_${resolutionName}.png`);
});
test(`Should render markdown image properly on ${resolutionName}`, async ({ page }) => {
diff --git a/test/page/teach/courses-page.ts b/test/page/teach/courses-page.ts
new file mode 100644
index 00000000000..dc4d3dbe39f
--- /dev/null
+++ b/test/page/teach/courses-page.ts
@@ -0,0 +1,21 @@
+import { Page } from '@playwright/test';
+import { GlobalSearch } from '../../component/global-search';
+import { PageWithGlobalSearch } from '../page-with-global-search';
+import { testSelector } from '../../utils';
+
+export class CoursesPage implements PageWithGlobalSearch {
+ readonly page: Page;
+ readonly globalSearch: GlobalSearch;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.globalSearch = new GlobalSearch(this.page);
+ }
+
+ async init() {
+ await this.page.goto('/education/courses.html');
+
+ // Wait for the page to load
+ await this.page.waitForSelector(testSelector('teach-courses'));
+ }
+}
\ No newline at end of file
diff --git a/test/page/teach-page.ts b/test/page/teach/education.ts
similarity index 66%
rename from test/page/teach-page.ts
rename to test/page/teach/education.ts
index acd09c6d76f..09a223b1816 100644
--- a/test/page/teach-page.ts
+++ b/test/page/teach/education.ts
@@ -1,13 +1,13 @@
import { Page } from '@playwright/test';
-import { testSelector } from '../utils';
-import { GlobalSearch } from '../component/global-search';
-import { PageWithGlobalSearch } from './page-with-global-search';
+import { testSelector } from '../../utils';
+import { GlobalSearch } from '../../component/global-search';
+import { PageWithGlobalSearch } from '../page-with-global-search';
export class TeachPage implements PageWithGlobalSearch {
readonly page: Page;
readonly globalSearch: GlobalSearch;
- constructor(page) {
+ constructor(page: Page) {
this.page = page;
this.globalSearch = new GlobalSearch(this.page);
}
diff --git a/test/page/teach/why-page.ts b/test/page/teach/why-page.ts
new file mode 100644
index 00000000000..432924b96dd
--- /dev/null
+++ b/test/page/teach/why-page.ts
@@ -0,0 +1,21 @@
+import { Page } from '@playwright/test';
+import { GlobalSearch } from '../../component/global-search';
+import { PageWithGlobalSearch } from '../page-with-global-search';
+import { testSelector } from '../../utils';
+
+export class WhyTeachPage implements PageWithGlobalSearch {
+ readonly page: Page;
+ readonly globalSearch: GlobalSearch;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.globalSearch = new GlobalSearch(this.page);
+ }
+
+ async init() {
+ await this.page.goto('/education/why-teach-kotlin.html');
+
+ // Wait for the page to load
+ await this.page.waitForSelector(testSelector('teach-why-teach-page'));
+ }
+}
\ No newline at end of file
diff --git a/test/production/global-search.spec.ts b/test/production/global-search.spec.ts
index 22248bda9a0..c2682cd4ea8 100644
--- a/test/production/global-search.spec.ts
+++ b/test/production/global-search.spec.ts
@@ -1,8 +1,8 @@
-import { test } from '@playwright/test';
+import { Page, test } from '@playwright/test';
import { IndexPage } from '../page/index-page';
import { CommunityPage } from '../page/community-page';
-import { TeachPage } from '../page/teach-page';
+import { TeachPage } from '../page/teach/education';
import { closeCookiesConsentBanner } from '../utils';
const SEARCH_STRING = 'Community';
@@ -10,22 +10,22 @@ const SEARCH_STRING = 'Community';
const pagesWithGlobalSearch = [
{
name: 'Index',
- getInstance: (page) => new IndexPage(page),
+ getInstance: (page: Page) => new IndexPage(page)
},
{
name: 'Community',
- getInstance: (page) => new CommunityPage(page),
+ getInstance: (page: Page) => new CommunityPage(page)
},
// {
// name: 'Teach',
- // getInstance: (page) => new TeachPage(page),
- // },
+ // getInstance: (page: Page) => new TeachPage(page)
+ // }
];
test.describe.configure({ mode: 'parallel' });
test.describe('Global Search Component', async () => {
- test.beforeEach(async ({ context , baseURL}) => {
+ test.beforeEach(async ({ context, baseURL }) => {
await closeCookiesConsentBanner(context, baseURL);
});
diff --git a/test/snapshots/test/e2e/teach/courses.spec.ts-snapshots/connect-us-chromium-darwin.png b/test/snapshots/test/e2e/teach/courses.spec.ts-snapshots/connect-us-chromium-darwin.png
new file mode 100644
index 00000000000..de60f226fb3
Binary files /dev/null and b/test/snapshots/test/e2e/teach/courses.spec.ts-snapshots/connect-us-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/courses.spec.ts-snapshots/sticky-menu-chromium-darwin.png b/test/snapshots/test/e2e/teach/courses.spec.ts-snapshots/sticky-menu-chromium-darwin.png
new file mode 100644
index 00000000000..14e2d77e9b0
Binary files /dev/null and b/test/snapshots/test/e2e/teach/courses.spec.ts-snapshots/sticky-menu-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/connect-us-chromium-darwin.png b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/connect-us-chromium-darwin.png
new file mode 100644
index 00000000000..375b47102a7
Binary files /dev/null and b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/connect-us-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/launch-course-text-chromium-darwin.png b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/launch-course-text-chromium-darwin.png
new file mode 100644
index 00000000000..6186aca2cf4
Binary files /dev/null and b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/launch-course-text-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/sticky-menu-chromium-darwin.png b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/sticky-menu-chromium-darwin.png
new file mode 100644
index 00000000000..de96ab429a7
Binary files /dev/null and b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/sticky-menu-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/subscription-form-chromium-darwin.png b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/subscription-form-chromium-darwin.png
new file mode 100644
index 00000000000..59247284574
Binary files /dev/null and b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/subscription-form-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/teach-features-chromium-darwin.png b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/teach-features-chromium-darwin.png
new file mode 100644
index 00000000000..78a17950de2
Binary files /dev/null and b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/teach-features-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/teach-logos-chromium-darwin.png b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/teach-logos-chromium-darwin.png
new file mode 100644
index 00000000000..a306e53e8fa
Binary files /dev/null and b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/teach-logos-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/teach-top-mobile-buttons-chromium-darwin.png b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/teach-top-mobile-buttons-chromium-darwin.png
new file mode 100644
index 00000000000..a5be93dcf9a
Binary files /dev/null and b/test/snapshots/test/e2e/teach/education.spec.ts-snapshots/teach-top-mobile-buttons-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/why.spec.ts-snapshots/connect-us-chromium-darwin.png b/test/snapshots/test/e2e/teach/why.spec.ts-snapshots/connect-us-chromium-darwin.png
new file mode 100644
index 00000000000..7226115f708
Binary files /dev/null and b/test/snapshots/test/e2e/teach/why.spec.ts-snapshots/connect-us-chromium-darwin.png differ
diff --git a/test/snapshots/test/e2e/teach/why.spec.ts-snapshots/sticky-menu-chromium-darwin.png b/test/snapshots/test/e2e/teach/why.spec.ts-snapshots/sticky-menu-chromium-darwin.png
new file mode 100644
index 00000000000..34b47dad221
Binary files /dev/null and b/test/snapshots/test/e2e/teach/why.spec.ts-snapshots/sticky-menu-chromium-darwin.png differ
diff --git a/test/utils.ts b/test/utils.ts
index 06a8232f961..7a90ac51303 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -1,6 +1,6 @@
import { BrowserContext } from '@playwright/test';
-export const testSelector = (name) => `[data-test="${name}"]`;
+export const testSelector = (name: string) => `[data-test="${name}"]`;
export function isStaging(baseURL: string): boolean {
const { hostname } = new URL(baseURL);