Skip to content

Commit

Permalink
Runs E2E test suite (#106)
Browse files Browse the repository at this point in the history
* Add basic test for runs (no experiments selected)

* Add test for selected generated experiment

* Update playwright dev dependency for local execution

* Test navigating from table to experiment page

* Test navigating from dashboard to runs

* Clean up tests

* Test navigation to run page on clicking run name

* Test that run page param searchbox works

* Test metrics explorer link query content

* Test Run settings page description/name editing

* Test that run selector is working

* Fix run selector test (force alphabetical ordering)

* Improve formatting and comments
  • Loading branch information
jescalada authored Jul 17, 2024
1 parent 56bdd87 commit c91569f
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 29 deletions.
144 changes: 144 additions & 0 deletions src/e2e/RunsExplorer/RunPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { expect, test } from '@playwright/test';

const BASE_URL = 'http://localhost:3000/aim';
const RUNS_PAGE = `${BASE_URL}/runs`;

test.describe('Individual Run Page', () => {
// Navigate to the run page before each test as the link's hash is unknown
test.beforeEach(async ({ page }) => {
await page.goto(RUNS_PAGE);
await page.waitForLoadState('networkidle');
await page.click(
'.ExperimentBar__headerContainer__appBarTitleBox__buttonSelectToggler',
);

const button = page.locator('button', { hasText: /^experiment-/ });
await button.click();
await page.waitForSelector('.Table__cell.undefined.experiment');
await page.mouse.click(0, 0);
await page.waitForSelector('.RunsTable');
const runNameLinks = page.locator(
'.Table__cell.undefined.run .RunNameColumn__runName a',
);

// Get the first run's link in alphabetical order and click on it
const runNameTexts = await runNameLinks.allTextContents();
const sortedRunNames = runNameTexts
.slice()
.sort((a, b) => a.localeCompare(b));

const firstRunNameIndex = runNameTexts.indexOf(sortedRunNames[0]);
const firstRunNameLink = runNameLinks.nth(firstRunNameIndex);

await firstRunNameLink.click();
await page.waitForURL(/\/aim\/runs\/.+/);
});

test('run params search box is working', async ({ page }) => {
await page.fill('input[name="search"]', 'param1');

await page.waitForSelector('div[role="row"]');

const row = await page.$eval('div[role="row"]', (row) => row.textContent);

expect(row).not.toBeNull();
expect(row).toMatch(/param1"(\d+\.\d+)"/);

const rows = await page.$$eval('div[role="row"]', (rows) =>
rows.map((row) => row.textContent),
);
expect(rows[0]).toContain('param1');
expect(rows.length).toBe(2); // 1 entry plus the header
});

test('view in metrics explorer link queries the right hash', async ({
page,
}) => {
// Wait for page content to load first
await page.waitForSelector('.runHashListItem__hashWrapper .Text');
const hash = await page.$eval(
'.runHashListItem__hashWrapper .Text',
(element) => element.textContent?.trim(),
);

await page.click('a:has-text("View in Metrics Explorer")', { force: true });

await page.waitForSelector('.view-lines.monaco-mouse-cursor-text');
const textBox = page.locator('.view-lines.monaco-mouse-cursor-text');

const textContent = await textBox.evaluate((el) => el.innerText);

// Remove tabs and newlines that might be present in the text content
const trimmedTextContent = textContent.replace(/\s/g, '');

expect(trimmedTextContent).toContain(hash);
});

test('settings page name and description can be edited', async ({ page }) => {
await page.click(
'.RunDetail__runDetailContainer__appBarContainer__appBarBox__actionContainer button',
);
await page.waitForSelector('.RunDetailSettingsTab');

const nameInput = page.locator(
'.NameAndDescriptionCard__content__nameBox__nameInput input',
);
await nameInput.fill('New Run Name');

const descriptionInput = page
.locator(
'.NameAndDescriptionCard__content__descriptionBox__descriptionInput textarea',
)
.first();
await descriptionInput.fill('New Run Description');

await page.click('.NameAndDescriptionCard__saveBtn', { force: true });
});

test('run selector is working', async ({ page }) => {
const runNameElement = page.locator(
'.RunDetail__runDetailContainer__appBarContainer__appBarTitleBox__runName',
);

const originalRunName = await runNameElement.innerText();

await page.click(
'.RunDetail__runDetailContainer__appBarContainer__appBarTitleBox__buttonSelectToggler',
{ force: true },
);

// Wait for the runs list to appear
await page.waitForSelector(
'div.RunSelectPopoverWrapper__selectPopoverContent__contentContainer__runsListContainer__runsList',
);

const runLocator = page.locator(
'a.RunSelectPopoverWrapper__selectPopoverContent__contentContainer__runsListContainer__runsList__runBox',
);

// Get the last run name in alphabetical order and click on it
const runNameElements = await runLocator.elementHandles();
const runNames = await Promise.all(
runNameElements.map((element) => element.innerText()),
);
const sortedRunNames = runNames.slice().sort((a, b) => a.localeCompare(b));
const lastRunNameIndex = runNames.indexOf(
sortedRunNames[sortedRunNames.length - 1],
);

const lastRunNameLink = runLocator.nth(lastRunNameIndex);
await lastRunNameLink.click({ force: true });

// Click outside the popover to close it
await page.mouse.click(0, 0);

const newRunName = await runNameElement.innerText();
expect(newRunName).not.toBe(originalRunName);
});

test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
await page.screenshot({ path: `failed-${testInfo.title}.png` });
}
});
});
109 changes: 109 additions & 0 deletions src/e2e/RunsExplorer/RunsExplorer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { test, expect } from '@playwright/test';

const BASE_URL = 'http://localhost:3000/aim';
const RUNS_PAGE = `${BASE_URL}/runs`;

test.describe('Runs Explorer', () => {
test.beforeEach(async ({ page }) => {
await page.goto(RUNS_PAGE);
await page.waitForLoadState('networkidle');
});

test('navigates correctly from dashboard', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForLoadState('networkidle');
await page.click('text=Runs');
await page.waitForLoadState('networkidle');
expect(page.url()).toBe(RUNS_PAGE);
});

test('displays no results message when no experiments are selected', async ({
page,
}) => {
const runsTable = page.locator('.RunsTable');
expect(await runsTable.locator('text=No results').count()).toBe(1);
});

test('has two runs if generated experiment is selected', async ({ page }) => {
// Add the generated experiment to the selection and wait for the table to appear
await page.click(
'.ExperimentBar__headerContainer__appBarTitleBox__buttonSelectToggler',
);

const button = page.locator('button', { hasText: /^experiment-/ });
await button.click();

await page.waitForSelector('.Table__cell.undefined.experiment');

const rows = page.locator(
'.Table__cell.undefined.experiment .ExperimentNameBox__experimentName',
);

expect(await rows.count()).toBe(2);

for (let i = 0; i < 2; i++) {
const runName = await rows.nth(i).innerText();
expect(runName).toMatch(/^experiment-/);
}
});

test('clicking on the experiment name navigates to the experiment page', async ({
page,
}) => {
await page.click(
'.ExperimentBar__headerContainer__appBarTitleBox__buttonSelectToggler',
);

const button = page.locator('button', { hasText: /^experiment-/ });
await button.click();

await page.waitForSelector('.Table__cell.undefined.experiment');

// Click on a random part of the screen to close the popover
await page.mouse.click(0, 0);

const rows = page.locator(
'.Table__cell.undefined.experiment .ExperimentNameBox__experimentName',
);

await rows
.locator('a', { hasText: /^experiment-/ })
.first()
.click();

await page.waitForURL(/\/aim\/experiments\/\d+\/overview/);
expect(page.url()).toMatch(/\/aim\/experiments\/\d+\/overview/);
});

test("clicking on the first run name navigates to that run's page", async ({
page,
}) => {
await page.click(
'.ExperimentBar__headerContainer__appBarTitleBox__buttonSelectToggler',
);

const button = page.locator('button', { hasText: /^experiment-/ });
await button.click();

await page.waitForSelector('.Table__cell.undefined.experiment');

await page.mouse.click(0, 0);

await page.waitForSelector('.RunsTable');

const firstRunNameLink = page
.locator('.Table__cell.undefined.run .RunNameColumn__runName a')
.first();

await firstRunNameLink.click();

await page.waitForURL(/\/aim\/runs\/.+/);
expect(page.url()).toMatch(/\/aim\/runs\/.+/);
});

test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
await page.screenshot({ path: `failed-${testInfo.title}.png` });
}
});
});
38 changes: 10 additions & 28 deletions src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
]
},
"devDependencies": {
"@playwright/test": "^1.44.1",
"@playwright/test": "^1.45.0",
"@storybook/addon-actions": "^6.5.12",
"@storybook/addon-essentials": "^6.5.12",
"@storybook/addon-interactions": "^6.5.12",
Expand Down

0 comments on commit c91569f

Please sign in to comment.