Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ generated
/data/page_views_map.json
test-results

public/data
public/data
test/storage-state.json
1 change: 1 addition & 0 deletions .teamcity/tests/buildTypes/E2EProductionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ object E2EProductionTest : BuildType({

features {
notifications {
branchFilter = "+:master"
enabled = !isProjectPlayground()
notifierSettings = slackNotifier {
connection = "PROJECT_EXT_486"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 15 additions & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 0 additions & 2 deletions test/e2e/teach/courses.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
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 }) => {
Expand Down
4 changes: 1 addition & 3 deletions test/e2e/teach/education.spec.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand Down
4 changes: 1 addition & 3 deletions test/e2e/teach/why.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 }) => {
Expand Down
14 changes: 2 additions & 12 deletions test/e2e/utils.ts
Original file line number Diff line number Diff line change
@@ -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<Buffer | undefined> {
Expand All @@ -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"]'),
Expand Down
56 changes: 56 additions & 0 deletions test/global-setup.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check how the order in which tests are run affects the closing of a cookie banner.

For example, if we run a test for a cookie banner, it should reset the previously saved state (when the banner was closed) and check that it exists. This means we'll save the state when the banner should be open.

If we then run another test where the banner should already be closed, will it pass?

Original file line number Diff line number Diff line change
@@ -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();
}
}
2 changes: 0 additions & 2 deletions test/production/api-navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 0 additions & 2 deletions test/production/community-kotlin-user-groups.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
2 changes: 0 additions & 2 deletions test/production/community-overview-rest-buttons.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
44 changes: 44 additions & 0 deletions test/production/cookie-banner.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
});
2 changes: 0 additions & 2 deletions test/production/footer-kotlin-ecosystem-buttons.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
2 changes: 0 additions & 2 deletions test/production/footer-social-media-buttons.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
8 changes: 0 additions & 8 deletions test/production/global-search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 }) => {
Expand Down
4 changes: 0 additions & 4 deletions test/production/grammar.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
13 changes: 3 additions & 10 deletions test/production/landings.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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/');
Expand Down
2 changes: 0 additions & 2 deletions test/production/main-page-buttons.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
2 changes: 0 additions & 2 deletions test/production/play-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 0 additions & 2 deletions test/production/solutions-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 0 additions & 2 deletions test/production/teach.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
Loading