Skip to content

Commit

Permalink
Revert "Revert "speed up ci builds from 15 to <7 minutes"" (#1643)
Browse files Browse the repository at this point in the history
Reverts #1639
  • Loading branch information
joeyorlando authored Mar 28, 2023
1 parent b9b7f9a commit 4857e74
Show file tree
Hide file tree
Showing 17 changed files with 117 additions and 106 deletions.
5 changes: 0 additions & 5 deletions integration-tests/alerts/onCallSchedule.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { test } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { verifyThatAlertGroupIsTriggered } from '../utils/alertGroup';
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
import { generateRandomValue } from '../utils/forms';
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
import { createOnCallSchedule } from '../utils/schedule';

test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});

test('we can create an oncall schedule + receive an alert', async ({ page }) => {
const escalationChainName = generateRandomValue();
const integrationName = generateRandomValue();
Expand Down
5 changes: 0 additions & 5 deletions integration-tests/alerts/sms.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { GRAFANA_USERNAME } from '../utils/constants';
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
import { generateRandomValue } from '../utils/forms';
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
import { waitForSms } from '../utils/phone';
import { configureUserNotificationSettings, verifyUserPhoneNumber } from '../utils/userSettings';

test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});

// TODO: enable once we've signed up for a MailSlurp account to receieve SMSes
test.skip('we can verify our phone number + receive an SMS alert', async ({ page }) => {
const escalationChainName = generateRandomValue();
Expand Down
25 changes: 10 additions & 15 deletions integration-tests/escalationChains/searching.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { test, expect, Page } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { generateRandomValue } from '../utils/forms';
import { createEscalationChain } from '../utils/escalationChain';

test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});

const assertEscalationChainSearchWorks = async (
page: Page,
searchTerm: string,
Expand All @@ -20,18 +15,18 @@ const assertEscalationChainSearchWorks = async (
await expect(page.getByTestId('escalation-chains-list')).toHaveText(escalationChainFullName);
};

test('searching allows case-insensitive partial matches', async ({ page }) => {
// TODO: add tests for the new filtering. Commented out as this search doesn't exist anymore
test.skip('searching allows case-insensitive partial matches', async ({ page }) => {
const escalationChainName = `${generateRandomValue()} ${generateRandomValue()}`;
// const [firstHalf, secondHalf] = escalationChainName.split(' ');
const [firstHalf, secondHalf] = escalationChainName.split(' ');

await createEscalationChain(page, escalationChainName);

// Commented as this search doesn't exist anymore TODO: add tests for the new filtering
// await assertEscalationChainSearchWorks(page, firstHalf, escalationChainName);
// await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName);
// await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName);
//
// await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName);
// await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName);
// await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, firstHalf, escalationChainName);
await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName);

await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName);
await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName);
});
42 changes: 40 additions & 2 deletions integration-tests/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
import { chromium, FullConfig, expect } from '@playwright/test';
import { chromium, FullConfig, expect, Page } from '@playwright/test';

import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME } from './utils/constants';
import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME, IS_OPEN_SOURCE, ONCALL_API_URL } from './utils/constants';
import { clickButton, getInputByName } from './utils/forms';
import { goToGrafanaPage } from './utils/navigation';

/**
* go to config page and wait for plugin icon to be available on left-hand navigation
*/
export const configureOnCallPlugin = async (page: Page): Promise<void> => {
// plugin configuration can safely be skipped for non open-source environments
if (!IS_OPEN_SOURCE) {
return;
}

/**
* go to the oncall plugin configuration page and wait for the page to be loaded
*/
await goToGrafanaPage(page, '/plugins/grafana-oncall-app');
await page.waitForSelector('text=Configure Grafana OnCall');

/**
* we may need to fill in the OnCall API URL if it is not set in the process.env
* of the frontend build
*/
const onCallApiUrlInput = getInputByName(page, 'onCallApiUrl');
const pluginIsAutoConfigured = (await onCallApiUrlInput.count()) === 0;

if (!pluginIsAutoConfigured) {
await onCallApiUrlInput.fill(ONCALL_API_URL);
await clickButton({ page, buttonText: 'Connect' });
}

// wait for the "Connected to OnCall" message to know that everything is properly configured
await expect(page.getByTestId('status-message-block')).toHaveText(/Connected to OnCall.*/);
};

/**
* Borrowed from our friends on the Incident team
Expand All @@ -20,6 +53,11 @@ const globalSetup = async (config: FullConfig): Promise<void> => {

expect(res.ok()).toBeTruthy();
await browserContext.storageState({ path: './storageState.json' });

// make sure the plugin has been configured
const page = await browserContext.newPage();
await configureOnCallPlugin(page);

await browserContext.close();
};

Expand Down
5 changes: 0 additions & 5 deletions integration-tests/integrations/uniqueIntegrationNames.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { openCreateIntegrationModal } from '../utils/integrations';

test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});

test('integrations have unique names', async ({ page }) => {
await openCreateIntegrationModal(page);

Expand Down
5 changes: 0 additions & 5 deletions integration-tests/schedules/addOverride.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { clickButton, generateRandomValue } from '../utils/forms';
import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule';
import dayjs from 'dayjs';

test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});

test('default dates in override creation modal are correct', async ({ page }) => {
const onCallScheduleName = generateRandomValue();
await createOnCallSchedule(page, onCallScheduleName);
Expand Down
13 changes: 6 additions & 7 deletions integration-tests/schedules/quality.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { generateRandomValue } from '../utils/forms';
import { createOnCallSchedule } from '../utils/schedule';

test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});

test('check schedule quality for simple 1-user schedule', async ({ page }) => {
const onCallScheduleName = generateRandomValue();
await createOnCallSchedule(page, onCallScheduleName);

await expect(page.locator('div[class*="ScheduleQuality"]')).toHaveText('Quality: Great');

await page.hover('div[class*="ScheduleQuality"]');
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=2 ')).toHaveText('Schedule has no gaps');
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText('Schedule is perfectly balanced');
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=2 ')).toHaveText(
'Schedule has no gaps'
);
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText(
'Schedule is perfectly balanced'
);
});
38 changes: 0 additions & 38 deletions integration-tests/utils/configurePlugin.ts

This file was deleted.

12 changes: 10 additions & 2 deletions integration-tests/utils/escalationChain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Page } from '@playwright/test';
import { expect, Page } from '@playwright/test';

import { clickButton, fillInInput, selectDropdownValue } from './forms';
import { goToOnCallPage } from './navigation';
Expand All @@ -22,6 +22,14 @@ export const createEscalationChain = async (
// go to the escalation chains page
await goToOnCallPage(page, 'escalations');

/**
* wait for Esclation Chains page to fully load. this is because this can change which "New Escalation Chain"
* button is present
* ie. the one on the left hand side in the list vs the one in the center when no escalation chains exist
*/
await page.getByTestId('page-title').locator('text=Escalation Chains').waitFor({ state: 'visible' });
await page.locator('text=Loading...').waitFor({ state: 'detached' });

// open the create escalation chain modal
(await page.waitForSelector('text=New Escalation Chain')).click();

Expand All @@ -30,7 +38,7 @@ export const createEscalationChain = async (

// submit the form and wait for it to be created
await clickButton({ page, buttonText: 'Create' });
await page.waitForSelector(`text=${escalationChainName}`);
await expect(page.getByTestId('escalation-chain-name')).toHaveText(escalationChainName);

if (!escalationStep || !escalationStepValue) {
return;
Expand Down
14 changes: 12 additions & 2 deletions integration-tests/utils/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const openSelect = async ({
placeholderText,
selectType,
startingLocator,
}: SelectDropdownValueArgs): Promise<void> => {
}: SelectDropdownValueArgs): Promise<Locator> => {
/**
* we currently mix three different dropdown components in the UI..
* so we need to support all of them :(
Expand All @@ -73,6 +73,8 @@ const openSelect = async ({
const selectElement: Locator = (startingLocator || page).locator(selector);
await selectElement.waitFor({ state: 'visible' });
await selectElement.click();

return selectElement;
};

/**
Expand All @@ -85,7 +87,15 @@ const chooseDropdownValue = async ({ page, value, optionExactMatch = true }: Sel
page.locator(`div[id^="react-select-"][id$="-listbox"] >> ${textMatchSelector(optionExactMatch, value)}`).click();

export const selectDropdownValue = async (args: SelectDropdownValueArgs): Promise<void> => {
await openSelect(args);
const selectElement = await openSelect(args);

/**
* use the select search to filter down the options
* TODO: get rid of the slice when we fix the GSelect component..
* without slicing this would fire off an API request for every key-stroke
*/
await selectElement.type(args.value.slice(0, 5));

await chooseDropdownValue(args);
};

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"@grafana/eslint-config": "^5.0.0",
"@grafana/toolkit": "^9.2.4",
"@jest/globals": "^27.5.1",
"@playwright/test": "^1.28.0",
"@playwright/test": "^1.32.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "12",
"@testing-library/user-event": "^14.4.3",
Expand Down
9 changes: 6 additions & 3 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const config: PlaywrightTestConfig = {
testDir: './integration-tests',
globalSetup: './integration-tests/globalSetup.ts',
/* Maximum time one test can run for. */
timeout: 60 * 1000,
// TODO: set this back to 60 when GSelect component is refactored
timeout: 90 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
Expand All @@ -27,8 +28,10 @@ const config: PlaywrightTestConfig = {
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
retries: process.env.CI ? 1 : 0,
// TODO: when GSelect component is refactored, run using 3 workers
// locally use one worker, on CI use 3
// workers: process.env.CI ? 3 : 1,
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
Expand Down
6 changes: 5 additions & 1 deletion src/PluginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ function RealPlugin(props: AppPluginPageProps): React.ReactNode {
{/* Render alerts at the top */}
<Alerts />
<Header backendLicense={store.backendLicense} />
{pages[page]?.text && !pages[page]?.hideTitle && <h3 className="page-title">{pages[page].text}</h3>}
{pages[page]?.text && !pages[page]?.hideTitle && (
<h3 className="page-title" data-testid="page-title">
{pages[page].text}
</h3>
)}
{props.children}
</RealPluginPage>
);
Expand Down
2 changes: 0 additions & 2 deletions src/components/Policy/EscalationPolicy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
className={cx('select', 'control')}
value={notify_schedule}
onChange={this._getOnChangeHandler('notify_schedule')}
fromOrganization
getOptionLabel={(item: SelectableValue) => {
const team = teamStore.items[scheduleStore.items[item.value].team];
return (
Expand Down Expand Up @@ -351,7 +350,6 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
className={cx('select', 'control')}
value={custom_button_trigger}
onChange={this._getOnChangeHandler('custom_button_trigger')}
fromOrganization
getOptionLabel={(item: SelectableValue) => {
const team = teamStore.items[outgoingWebhookStore.items[item.value].team];
return (
Expand Down
11 changes: 9 additions & 2 deletions src/containers/GSelect/GSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { observer } from 'mobx-react';
import { useStore } from 'state/useStore';

import styles from './GSelect.module.css';
// import { debounce } from 'lodash';

const cx = cn.bind(styles);

Expand All @@ -30,7 +31,6 @@ interface GSelectProps {
showWarningIfEmptyValue?: boolean;
showError?: boolean;
nullItemName?: string;
fromOrganization?: boolean;
filterOptions?: (id: any) => boolean;
dropdownRender?: (menu: ReactElement) => ReactElement;
getOptionLabel?: <T>(item: SelectableValue<T>) => React.ReactNode;
Expand Down Expand Up @@ -61,7 +61,6 @@ const GSelect = observer((props: GSelectProps) => {
showWarningIfEmptyValue = false,
getDescription,
filterOptions,
// fromOrganization,
width = null,
icon = null,
} = props;
Expand Down Expand Up @@ -89,6 +88,11 @@ const GSelect = observer((props: GSelectProps) => {
[model, onChange]
);

/**
* without debouncing this function when search is available
* we risk hammering the API endpoint for every single key stroke
* some context on 250ms as the choice here - https://stackoverflow.com/a/44755058/3902555
*/
const loadOptions = (query: string) => {
return model.updateItems(query).then(() => {
const searchResult = model.getSearchResult(query);
Expand All @@ -106,6 +110,9 @@ const GSelect = observer((props: GSelectProps) => {
});
};

// TODO: why doesn't this work properly?
// const loadOptions = debounce(_loadOptions, showSearch ? 250 : 0);

const values = isMulti
? (value ? (value as string[]) : [])
.filter((id) => id in model.items)
Expand Down
Loading

0 comments on commit 4857e74

Please sign in to comment.