diff --git a/.gitignore b/.gitignore index 728327101da..a72efb9eec8 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,5 @@ generated /data/page_views_map.json test-results -public/data \ No newline at end of file +public/data +test/storage-state.json 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" diff --git a/package.json b/package.json index a5a12cdf569..f76f8025aad 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 2403db5bfff..7d86945e020 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,17 +2,28 @@ import { defineConfig, devices } from '@playwright/test'; const MAX_DIFF_PIXEL_RATIO = 0.025 as const; +const isDevelopment = !process.env.CI; + +const reporter = isDevelopment ? 'list' : 'playwright-teamcity-reporter'; +const retries = isDevelopment ? 0 : 2; +const timeout = isDevelopment ? 10000 : 5000; + +const forbidOnly = !isDevelopment; + export default defineConfig({ - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - reporter: process.env.CI ? 'dot' : 'list', + globalSetup: require.resolve('./test/global-setup.ts'), + forbidOnly, + retries, + reporter, snapshotDir: 'test/snapshots', expect: { + timeout, toMatchSnapshot: { maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO }, toHaveScreenshot: { maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO } }, use: { - baseURL: process.env.BASE_URL || 'http://localhost:9000', + baseURL: process.env.BASE_URL || 'http://localhost:3000', + storageState: 'test/storage-state.json', trace: 'off', screenshot: 'only-on-failure', video: 'retain-on-failure' diff --git a/test/e2e/teach/courses.spec.ts b/test/e2e/teach/courses.spec.ts index a3612181737..00ac5e62adf 100644 --- a/test/e2e/teach/courses.spec.ts +++ b/test/e2e/teach/courses.spec.ts @@ -1,6 +1,5 @@ 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'; @@ -8,7 +7,6 @@ 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 }) => { diff --git a/test/e2e/teach/education.spec.ts b/test/e2e/teach/education.spec.ts index fa1fe172bae..160b22c1c47 100644 --- a/test/e2e/teach/education.spec.ts +++ b/test/e2e/teach/education.spec.ts @@ -1,14 +1,12 @@ 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 }) => { + test.beforeEach(async ({ page }) => { const teachPage = new TeachPage(page); await teachPage.init(); - await closeExternalBanners(context, page, baseURL); }); 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..8a5482b203c 100644 --- a/test/e2e/teach/why.spec.ts +++ b/test/e2e/teach/why.spec.ts @@ -1,6 +1,5 @@ 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'; @@ -21,10 +20,9 @@ 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); }); 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..b9e4021ddc3 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 { isSkipScreenshot } from '../utils'; import { PageAssertionsToHaveScreenshotOptions } from 'playwright/types/test'; export async function getElementScreenshotWithPadding(page: Page, element: ElementHandle, padding: number): Promise { @@ -18,16 +18,6 @@ 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 function pageWrapperMask(page: Page) { return [ page.locator('header[data-test="header"]'), diff --git a/test/global-setup.ts b/test/global-setup.ts new file mode 100644 index 00000000000..2ee1307474f --- /dev/null +++ b/test/global-setup.ts @@ -0,0 +1,56 @@ +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'); + + const project = config.projects[0]; + console.log(`[Global Setup] Processing project ${project.name}`); + const baseURL = project.use?.baseURL; + + if (isProduction(baseURL)) { + await closeProductionElements(baseURL, storageStatePath); + } +} + +async function closeProductionElements(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, { waitUntil: 'domcontentloaded' }); + + 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'); + } + + const closeBanner = page.locator('#optly-banner_close'); + + if (await closeBanner.count() > 0) { + console.log('[Global Setup] Closing "purple" banner'); + await closeBanner.click(); + await page.waitForSelector('#optly-banner_close', { state: 'hidden' }); + } + + 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(); + } +} \ No newline at end of file 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..3ebcb96e724 --- /dev/null +++ b/test/production/cookie-banner.spec.ts @@ -0,0 +1,44 @@ +import { expect, test as base } from '@playwright/test'; +import { skipNonProduction } from '../utils'; + +skipNonProduction('Cookie banner only on production'); + +const test = base.extend({ + page: async ({ browser }, use) => { + const context = await browser.newContext({ + storageState: undefined + }); + const page = await context.newPage(); + + await use(page); + + await page.close(); + await context.close(); + } +}); + +test.describe('Cookie banner functionality', () => { + const PAGE_TYPES_EXAMPLE = [ + '/', + '/docs/getting-started.html', + '/docs/multiplatform/get-started.html', + '/api/core/kotlin-stdlib/', + '/api/kotlinx.coroutines/kotlinx-coroutines-core/', + '/lp/multiplatform/case-studies/autodesk/' + ]; + + for (const path of PAGE_TYPES_EXAMPLE) { + test(`Cookie banner should be visible and closeable: ${path}`, async ({ page, baseURL }) => { + await page.goto(`${baseURL}${path}`); + + 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..9189e905271 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'; @@ -22,13 +20,7 @@ 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 2dc81bf971e..16b9c847e36 100644 --- a/test/production/grammar.spec.ts +++ b/test/production/grammar.spec.ts @@ -1,11 +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); diff --git a/test/production/landings.spec.ts b/test/production/landings.spec.ts index 3877481470b..2124bffe5d3 100644 --- a/test/production/landings.spec.ts +++ b/test/production/landings.spec.ts @@ -1,16 +1,10 @@ -import { test, expect } from '@playwright/test'; -import { closeCookiesConsentBanner, isStaging } from '../utils'; - -test.describe.configure({ mode: 'parallel' }); +import { expect, test } from '@playwright/test'; +import { isProduction } from '../utils'; test.describe('/lp/ pages list', async () => { - test.beforeEach(async ({ context, baseURL }) => { - await closeCookiesConsentBanner(context, baseURL); - }); + test.skip(({ baseURL }) => !isProduction(baseURL), 'for host with reverse-proxy only'); test(`Check /lp/multiplatform default redirects`, async ({ page, baseURL }) => { - test.skip(isStaging(baseURL), 'for host with reverse-proxy only'); - const targetUrl = 'https://kotlinlang.org/multiplatform/'; await page.goto('/lp/multiplatform'); @@ -24,7 +18,6 @@ test.describe('/lp/ pages list', async () => { }); test(`Check /lp/multiplatform case-studies redirect`, async ({ page, baseURL }) => { - test.skip(isStaging(baseURL), 'for host with reverse-proxy only'); 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..acbba5a94e6 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,21 +1,8 @@ -import { BrowserContext, expect, Locator, Page } from '@playwright/test'; +import { expect, Locator, Page, test } from '@playwright/test'; 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,15 @@ 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; + } +} + +export function skipNonProduction(message: string) { + test.skip(({ baseURL }) => !isProduction(baseURL), message); +} diff --git a/yarn.lock b/yarn.lock index 11802348e6b..dee3e0d5556 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"