Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] Playwright initial setup #212970

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
23 changes: 23 additions & 0 deletions x-pack/solutions/observability/plugins/apm/ui_tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## How to run tests

First start the servers:

```bash
// ESS
node scripts/scout.js start-server --stateful

// Serverless
node scripts/scout.js start-server --serverless=[es|oblt|security]
```

Then you can run the parallel tests in another terminal:

```bash
// ESS
npx playwright test --config x-pack/solutions/observability/plugins/apm/ui_tests/parallel.playwright.config.ts --grep @ess

// Serverless
npx playwright test --config x-pack/solutions/observability/plugins/apm/ui_tests/parallel.playwright.config.ts --grep @svlOblt
```

Test results are available in `x-pack/solutions/observability/plugins/apm/ui_tests/output`
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { PageObjects, ScoutTestFixtures, ScoutWorkerFixtures, KibanaUrl } from '@kbn/scout';
import { test as base, createLazyPageObject } from '@kbn/scout';
import { ServiceMapPage } from './page_objects/service_map';
import { ServiceInventoryPage } from './page_objects/service_inventory';

export interface ExtendedScoutTestFixtures extends ScoutTestFixtures {
pageObjects: PageObjects & {
serviceMapPage: ServiceMapPage;
serviceInventoryPage: ServiceInventoryPage;
};
}

export const test = base.extend<ExtendedScoutTestFixtures, ScoutWorkerFixtures>({
pageObjects: async (
{
pageObjects,
page,
kbnUrl,
}: {
pageObjects: ExtendedScoutTestFixtures['pageObjects'];
page: ExtendedScoutTestFixtures['page'];
kbnUrl: KibanaUrl;
},
use: (pageObjects: ExtendedScoutTestFixtures['pageObjects']) => Promise<void>
) => {
const extendedPageObjects = {
...pageObjects,
serviceMapPage: createLazyPageObject(ServiceMapPage, page, kbnUrl),
serviceInventoryPage: createLazyPageObject(ServiceInventoryPage, page, kbnUrl),
};

await use(extendedPageObjects);
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { KibanaUrl, ScoutPage } from '@kbn/scout';

export class ServiceInventoryPage {
constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {}

async waitForPageToLoad() {
await this.page.waitForSelector(
'[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]',
{ state: 'visible' }
);
}

async gotoDetailedServiceInventoryWithDateSelected(start: string, end: string) {
this.page.goto(`${this.kbnUrl.app('apm')}/services?&rangeFrom=${start}&rangeTo=${end}`);
await this.waitForPageToLoad();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { KibanaUrl, ScoutPage } from '@kbn/scout';

export class ServiceMapPage {
constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {}

async waitForPageToLoad() {
await this.page.waitForSelector(
'[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]',
{ state: 'visible' }
);
}

async gotoWithDateSelected(start: string, end: string) {
this.page.goto(`${this.kbnUrl.app('apm')}/service-map?&rangeFrom=${start}&rangeTo=${end}`);
await this.waitForPageToLoad();
}
async gotoDetailedServiceMapWithDateSelected(start: string, end: string) {
this.page.goto(
`${this.kbnUrl.app(
'apm'
)}/services/opbeans-java/service-map?&rangeFrom=${start}&rangeTo=${end}`
);
await this.waitForPageToLoad();
}
async getSearchBar() {
await this.page.testSubj.waitForSelector('apmUnifiedSearchBar');
}
async typeInTheSearchBar() {
await this.getSearchBar();
await this.page.testSubj.typeWithDelay('apmUnifiedSearchBar', '_id : foo');
await this.page.getByTestId('querySubmitButton').press('Enter');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { apm, timerange } from '@kbn/apm-synthtrace-client';

export function opbeans({ from, to }: { from: number; to: number }) {
const range = timerange(from, to);

const opbeansJava = apm
.service({
name: 'opbeans-java',
environment: 'production',
agentName: 'java',
})
.instance('opbeans-java-prod-1')
.podId('opbeans-java-prod-1-pod');

const opbeansNode = apm
.service({
name: 'opbeans-node',
environment: 'production',
agentName: 'nodejs',
})
.instance('opbeans-node-prod-1');

const opbeansRum = apm.browser({
serviceName: 'opbeans-rum',
environment: 'production',
userAgent: apm.getChromeUserAgentDefaults(),
});

return range
.interval('1s')
.rate(1)
.generator((timestamp) => [
opbeansJava
.transaction({ transactionName: 'GET /api/product' })
.timestamp(timestamp)
.duration(1000)
.failure()
.errors(
opbeansJava.error({ message: '[MockError] Foo', type: `Exception` }).timestamp(timestamp)
)
.children(
opbeansJava
.span({
spanName: 'SELECT * FROM product',
spanType: 'db',
spanSubtype: 'postgresql',
})
.timestamp(timestamp)
.duration(50)
.failure()
.destination('postgresql')
),
opbeansNode
.transaction({ transactionName: 'GET /api/product/:id' })
.timestamp(timestamp)
.duration(500)
.success(),
opbeansNode
.transaction({
transactionName: 'Worker job',
transactionType: 'Worker',
})
.timestamp(timestamp)
.duration(1000)
.success(),
opbeansRum.transaction({ transactionName: '/' }).timestamp(timestamp).duration(1000),
]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { createPlaywrightConfig } from '@kbn/scout';

// eslint-disable-next-line import/no-default-export
export default createPlaywrightConfig({
globalSetup: require.resolve('./parallel_tests/global_setup'),
testDir: './parallel_tests',
workers: 2,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { ingestSynthtraceDataHook } from '@kbn/scout';
import { type FullConfig } from '@playwright/test';
import { opbeans } from '../fixtures/synthtrace/opbeans';

async function globalSetup(config: FullConfig) {
const start = '2021-10-10T00:00:00.000Z';
const end = '2021-10-10T00:15:00.000Z';
const data = {
apm: [
opbeans({
from: new Date(start).getTime(),
to: new Date(end).getTime(),
}),
],
// TODO add infra and otel fixtures
infra: [],
otel: [],
};

return ingestSynthtraceDataHook(config, data);
}

// eslint-disable-next-line import/no-default-export
export default globalSetup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { expect } from '@kbn/scout';
import { test } from '../../fixtures';

const start = '2021-10-10T00:00:00.000Z';
const end = '2021-10-10T00:15:00.000Z';

test.describe('Service Inventory', { tag: ['@ess', '@svlOblt'] }, () => {
test.beforeEach(async ({ browserAuth, page, pageObjects: { serviceInventoryPage } }) => {
await browserAuth.loginAsViewer();
await serviceInventoryPage.gotoDetailedServiceInventoryWithDateSelected(start, end);
await page.waitForSelector(
'[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]',
{ state: 'visible' }
);
Comment on lines +17 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
await page.waitForSelector(
'[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]',
{ state: 'visible' }
);
await serviceInventoryPage.waitForPageToLoad();

});

test('shows the service inventory', async ({ page, pageObjects: { serviceInventoryPage } }) => {
await serviceInventoryPage.gotoDetailedServiceInventoryWithDateSelected(start, end);
expect(page.url()).toContain('/app/apm/services');
await expect(page.getByRole('heading', { name: 'Services', level: 1 })).toBeVisible();
});

test('shows a list of services', async ({ page }) => {
await expect(page.getByText('opbeans-node')).toBeVisible();
await expect(page.getByText('opbeans-java')).toBeVisible();
await expect(page.getByText('opbeans-rum')).toBeVisible();
});

test('shows a list of environments', async ({ page }) => {
const environmentEntrySelector = page.locator('td:has-text("production")');
await expect(environmentEntrySelector).toHaveCount(3);
});

test('loads the service overview for a service when clicking on it', async ({ page }) => {
await page.getByText('opbeans-node').click();
expect(page.url()).toContain('/apm/services/opbeans-node/overview');
await expect(page.getByTestId('apmMainTemplateHeaderServiceName')).toHaveText('opbeans-node');
});

test('shows the correct environment when changing the environment', async ({ page }) => {
await page
.locator('[data-test-subj="environmentFilter"]')
.locator('[data-test-subj="comboBoxSearchInput"]')
.click();
await expect(
page.getByTestId('comboBoxOptionsList environmentFilter-optionsList')
).toBeVisible();
await page
.locator('[data-test-subj="comboBoxOptionsList environmentFilter-optionsList"]')
.locator('button:has-text("production")')
.click();
await expect(page.getByTestId('comboBoxSearchInput')).toHaveValue('production');
});

test('shows the filtered services when using the service name fast filter', async ({ page }) => {
await expect(page.getByTestId('tableSearchInput')).toBeVisible();
await expect(page.getByText('opbeans-node')).toBeVisible();
await expect(page.getByText('opbeans-java')).toBeVisible();
await expect(page.getByText('opbeans-rum')).toBeVisible();
await page.getByTestId('tableSearchInput').fill('java');
await expect(page.getByText('opbeans-node')).toBeHidden();
await expect(page.getByText('opbeans-java')).toBeVisible();
await expect(page.getByText('opbeans-rum')).toBeHidden();
await page.getByTestId('tableSearchInput').clear();
await expect(page.getByText('opbeans-node')).toBeVisible();
await expect(page.getByText('opbeans-java')).toBeVisible();
await expect(page.getByText('opbeans-rum')).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { expect } from '@kbn/scout';
import { test } from '../../fixtures';

const start = '2021-10-10T00:00:00.000Z';
const end = '2021-10-10T00:15:00.000Z';

test.describe('Service Map', { tag: ['@ess', '@svlOblt'] }, () => {
test.beforeEach(async ({ browserAuth, page, pageObjects: { serviceMapPage } }) => {
await browserAuth.loginAsViewer();
await serviceMapPage.gotoWithDateSelected(start, end);
await page.waitForSelector(
'[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]',
{ state: 'visible' }
);
Comment on lines +18 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
await page.waitForSelector(
'[data-test-subj="kbnAppWrapper visibleChrome"] [aria-busy="false"]',
{ state: 'visible' }
);
await serviceMapPage.waitForPageToLoad();

});
test('shows the service map', async ({ page, pageObjects: { serviceMapPage } }) => {
await serviceMapPage.gotoWithDateSelected(start, end);
expect(page.url()).toContain('/app/apm/service-map');
await page.waitForSelector('[data-test-subj="serviceMap"]');
await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden();
await page.getByLabel('Zoom In').click();
await page.getByTestId('centerServiceMap').click();
await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden();
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.getByTestId('serviceMap').click();
const serviceMapLocator = await page.getByTestId('serviceMap');
await expect(serviceMapLocator).toHaveScreenshot('service_map.png', {
animations: 'disabled',
maxDiffPixels: 10,
});
});

test('shows a detailed service map', async ({ page, pageObjects: { serviceMapPage } }) => {
await serviceMapPage.gotoDetailedServiceMapWithDateSelected(start, end);
expect(page.url()).toContain('/services/opbeans-java/service-map');
await page.waitForSelector('[data-test-subj="serviceMap"]');
await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden();
await page.getByLabel('Zoom out').click();
await page.getByTestId('centerServiceMap').click();
await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden();
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.getByTestId('serviceMap').click();
const serviceMapLocator = await page.getByTestId('serviceMap');
await expect(serviceMapLocator).toHaveScreenshot('detailed_service_map.png', {
animations: 'disabled',
maxDiffPixels: 10,
});
});

test('shows empty state when there is no data', async ({
page,
pageObjects: { serviceMapPage },
}) => {
await serviceMapPage.typeInTheSearchBar();
await expect(page.getByTestId('serviceMap').getByLabel('Loading')).toBeHidden();
page.getByText('No services available');
// search bar is still visible
await expect(page.getByTestId('apmUnifiedSearchBar')).toBeVisible();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading