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
19 changes: 17 additions & 2 deletions e2e/crawl/crawl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ import { crawlRoutes } from './routes';
// the accessibility findings of axe, and a screenshot. The crawl only
// navigates and reads. It never clicks, types or submits anything and sends no
// request of its own besides loading the pages, so it changes nothing on the
// deployment. Findings never fail a test; only a screen that cannot be crawled
// does.
// deployment. Findings never fail a test; only a screen that cannot be crawled,
// or that rendered signed out, does.

const API = '/pulp/api/';
// The one screen that is meant to be seen without a session.
const LOGIN_ROUTE = '/login';
const USER_MENU = '[data-cy=user-dropdown]';
const SETTLE_MS = 15_000;
const AXE_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];
const account = crawlAccount();
Expand Down Expand Up @@ -154,6 +157,7 @@ for (const route of crawlRoutes()) {
const errorBoundary =
(await page.getByText('The stack trace is:').count()) > 0 ||
(await page.getByText('Something went horribly wrong!').count()) > 0;
const signedIn = (await page.locator(USER_MENU).count()) > 0;

const axe = await new AxeBuilder({ page }).withTags(AXE_TAGS).analyze();

Expand Down Expand Up @@ -184,6 +188,7 @@ for (const route of crawlRoutes()) {
account,
title: heading?.trim() || (await page.title()),
errorBoundary,
signedIn,
loadMs,
settled,
screenshot,
Expand Down Expand Up @@ -211,5 +216,15 @@ for (const route of crawlRoutes()) {
run: process.env['PULP_UI_CRAWL_RUN'] ?? '',
capturedAt: new Date().toISOString(),
});

// A screen that rendered signed out is the login page, not the screen the
// route names; a crawl of it would report a clean page that was never
// looked at. The report is written first so the screen stays on record.
if (route.pattern !== LOGIN_ROUTE) {
expect(
signedIn,
`the user menu of the ${account} account is shown on ${route.pattern}`,
).toBe(true);
}
});
}
4 changes: 4 additions & 0 deletions e2e/crawl/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const NAMED_LISTS: Record<string, string> = {
'/file/remotes': 'remotes/file/file/',
'/ansible/repositories': 'repositories/ansible/ansible/',
'/ansible/remotes': 'remotes/ansible/collection/',
'/rpm/repositories': 'repositories/rpm/rpm/',
'/rpm/remotes': 'remotes/rpm/rpm/',
'/python/repositories': 'repositories/python/python/',
'/python/remotes': 'remotes/python/python/',
};

function isCollectionRoute(route: CrawlRoute): boolean {
Expand Down
4 changes: 4 additions & 0 deletions e2e/crawl/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export interface ScreenReport {
account: Role;
title: string;
errorBoundary: boolean;
// Whether the page showed the signed-in user's menu. A screen that rendered
// signed out (an expired or unusable session) shows the login page and says
// nothing about the screen it was meant to visit.
signedIn: boolean;
loadMs: number;
// False when the page still showed a spinner or kept the network busy at
// the end of the wait.
Expand Down
1 change: 1 addition & 0 deletions e2e/crawl/summarize.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function report(route, slug, theme, viewport, run, extra = {}) {
account: 'admin',
title: 'T',
errorBoundary: false,
signedIn: true,
loadMs: 5,
settled: true,
screenshot: `${slug}.${theme}.${viewport}.png`,
Expand Down
1 change: 1 addition & 0 deletions e2e/crawl/verify-report.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const REQUIRED_FIELDS = [
'account',
'title',
'errorBoundary',
'signedIn',
'loadMs',
'screenshot',
'consoleErrors',
Expand Down
1 change: 1 addition & 0 deletions e2e/crawl/verify-report.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function screen(route, theme, viewport, slug) {
account: 'admin',
title: 'Status',
errorBoundary: false,
signedIn: true,
loadMs: 10,
screenshot: `${slug}.${theme}.${viewport}.png`,
consoleErrors: [],
Expand Down
64 changes: 64 additions & 0 deletions e2e/remote/list-order.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { expect, test } from '@playwright/test';
import { apiContext } from '../support/api';
import { title } from '../support/sign-in';

// A list opens ordered by name, the way the server orders it, so the first
// repository a person sees is the alphabetically first one, and a form says
// which field it cannot be saved without.

test('the file repository list opens with the repositories in the order the server sorts them by name', async ({
page,
}) => {
const api = await apiContext();
const names = async (params: Record<string, string>) => {
const response = await api.get('/pulp/api/v3/repositories/file/file/', {
params: { ...params, ordering: 'name', fields: 'name' },
});
expect(response.ok(), 'the API list').toBe(true);
return (
(await response.json()) as { results: { name: string }[] }
).results.map(({ name }) => name);
};

try {
await page.goto('file/repositories');
await expect(title(page, 'Repositories')).toBeVisible();
const links = page.locator('tbody tr td:first-child a');
await expect(links.first()).toBeVisible();

// Other specs create repositories while this one runs, so the rows shown
// are compared with the server's order of exactly those names, and the
// top row with the server's first name.
const shown = await links.allInnerTexts();
expect(shown.length, 'rows shown').toBeGreaterThan(1);
expect(shown, 'the rows, top to bottom').toEqual(
await names({ name__in: shown.join(','), limit: '100' }),
);
expect(shown[0], 'the top row').toBe((await names({ limit: '1' }))[0]);
} finally {
await api.dispose();
}
});

test('the file repository add form marks Name as required and keeps Save disabled while it is empty', async ({
page,
}) => {
await page.goto('file/repositories/edit/_');
await expect(title(page, 'Add new repository')).toBeVisible();

const nameGroup = page.locator('.pf-v5-c-form__group', {
has: page.locator('#name'),
});
await expect(
nameGroup.locator('.pf-v5-c-form__label-required'),
'the required marker of Name',
).toBeVisible();

const save = page.getByRole('button', { name: 'Save' });
await expect(save).toBeDisabled();

await page.locator('#name').fill('zz-e2e-not-saved');
await expect(save).toBeEnabled();
await page.locator('#name').fill('');
await expect(save).toBeDisabled();
});
32 changes: 25 additions & 7 deletions e2e/remote/list-pages.spec.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import { expect, test } from '@playwright/test';
import { title } from '../support/sign-in';
import { listPages, missingPlugin } from './list-pages';
import {
ALLOW_MISSING_PLUGINS_VARIABLE,
failureShown,
listPages,
missingExpectedPlugins,
missingPlugin,
missingPluginsAllowed,
watchPage,
} from './list-pages';

// Every list page of every installed plugin, and of Pulp core, opens for the
// admin account with its title, without the error boundary, without an error
// alert and without an uncaught error in the page.
// alert or error state and without an uncaught error in the page.

test.beforeAll(async () => {
if (missingPluginsAllowed()) {
return;
}
expect(
await missingExpectedPlugins(),
`plugins the server does not run (set ${ALLOW_MISSING_PLUGINS_VARIABLE} to skip their pages)`,
).toEqual([]);
});

for (const entry of listPages) {
test(`${entry.name} (${entry.path}) opens`, async ({ page }) => {
const absent = await missingPlugin(entry);
test.skip(absent !== null, absent ?? '');
test.fixme(!!entry.fixme?.admin, entry.fixme?.admin ?? '');
// runs anyway: the run reports it the day the page works
test.fail(!!entry.knownDefect?.admin, entry.knownDefect?.admin ?? '');

const errors: string[] = [];
page.on('pageerror', (error) => errors.push(error.message));
const watch = watchPage(page);

await page.goto(entry.path);
await expect(title(page, entry.title)).toBeVisible();
await page.waitForLoadState('networkidle');

await expect(page.getByText('The stack trace is:')).toHaveCount(0);
await expect(page.locator('.pf-v5-c-alert.pf-m-danger')).toHaveCount(0);
expect(errors, 'uncaught errors of the page').toEqual([]);
await expect(failureShown(page)).toHaveCount(0);
expect(watch.errors, 'uncaught errors of the page').toEqual([]);
});
}
Loading
Loading