forgejo/tests/e2e/runner-management.test.e2e.ts
Andreas Ahlenstorf 26f401cab4 fix: use CSS class to indicate that runner name is required (#11718)
Followup to https://codeberg.org/forgejo/forgejo/pulls/11516#issuecomment-11634760

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11718
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
Co-committed-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
2026-03-17 18:51:29 +01:00

738 lines
31 KiB
TypeScript

// @watch start
// modules/actions/**
// routers/web/shared/actions/**
// routers/web/web.go
// services/forms/runner.go
// templates/admin/runners/**
// templates/org/settings/runner*
// templates/repo/settings/runner*
// templates/shared/actions/runner*
// templates/user/settings/runner*
// web_src/css/actions.css
// @watch end
import {test} from './utils_e2e.ts';
import {expect} from '@playwright/test';
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const tokenPattern = /^[0-9a-f]{40}$/;
test.describe('Runners of user2', () => {
test.use({user: 'user2'});
test('usable runners are visible', async ({page}) => {
await page.goto('/user/settings/actions/runners');
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
const runnerContainer = page.locator('.runner-container');
const rows = runnerContainer.getByRole('row');
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
// the ordering of tests.
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toMatchAriaSnapshot(`
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
- cell "docker"
- cell "Individual"
- cell "Offline"
- cell "Show details of runner-2"
- cell "Edit runner-2"
- cell "Delete runner-2"
`);
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
- cell "docker"
- cell "Global"
- cell "Offline"
- cell "Show details of runner-4"
- cell
- cell
`);
await page.getByRole('link', {name: 'Show details of runner-2', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-2 .*/);
await page.goto('/user/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
});
test('runner details with tasks of repositories owned by user', async ({page}) => {
await page.goto('/user/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
await expect(page.getByLabel('Properties of runner-4')).toMatchAriaSnapshot(`
- term: UUID
- definition: ${uuidPattern}
- term: Type
- definition: Global
- term: Labels
- definition: docker
- term: Last online time
- definition: Never
- term: Status
- definition: Offline
- term: Ephemeral
- definition: "no"
- term: Description
- definition: A runner for everyone
`);
await expect(page.getByRole('heading', {name: 'Recent tasks of this user on this runner'})).toBeVisible();
const rows = page.getByRole('row');
// Only tasks from repositories owned by user2 should appear.
await expect(rows).toHaveCount(2);
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
await expect(rows.nth(1)).toHaveAccessibleName('88932 Waiting 49f55ab99b -');
});
test('create new runner', async ({page}) => {
await page.goto('/user/settings/actions/runners');
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
await page.getByRole('link', {name: 'Create new runner'}).click();
await expect(page).toHaveTitle(/^Create new runner .*/);
// Submit an invalid form to test validation.
await page.getByRole('button', {name: 'Create'}).click();
await expect(page.getByRole('paragraph')).toHaveText('Name cannot be empty.');
// Submit a valid form to create a runner.
await page.getByRole('textbox', {name: 'Name *'}).fill('runner-991301');
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-991301');
await page.getByRole('button', {name: 'Create'}).click();
// Verify set up instructions.
await expect(page).toHaveTitle(/^Set up runner runner-991301 .*/);
await expect(page.getByRole('heading', {name: 'Set up runner runner-991301'})).toBeVisible();
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
const runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerUUID).toMatch(uuidPattern);
await page.getByRole('button', {name: 'Copy runner token'}).click();
const runnerToken = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerToken).toMatch(tokenPattern);
await expect(page.getByRole('term')).toHaveText(['UUID', 'Token']);
await expect(page.getByRole('definition')).toContainText([runnerUUID, runnerToken]);
await expect(page.getByRole('heading', {name: 'Using the runner configuration file'})).toBeVisible();
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`uuid: ${runnerUUID}`);
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`token: ${runnerToken}`);
await expect(page.getByRole('heading', {name: 'Using program options'})).toBeVisible();
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--uuid ${runnerUUID}`);
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--token ${runnerToken}`);
// Go back to list of runners.
await page.getByRole('link', {name: 'List of runners', exact: true}).click();
await expect(page.locator(`tbody tr:has-text("${runnerUUID}")`)).toMatchAriaSnapshot(`
- cell "runner-991301 ${runnerUUID}"
- cell ""
- cell "Individual"
- cell "Offline"
- cell "Show details of runner-991301"
- cell "Edit runner-991301"
- cell "Delete runner-991301"
`);
});
test('edit runner without changing its token', async ({page}) => {
await page.goto('/user/settings/actions/runners');
// We have to create a new runner because changes to fixtures would affect the remainder of the tests in this file.
await page.getByRole('link', {name: 'Create new runner'}).click();
await page.getByRole('textbox', {name: 'Name *'}).fill('runner-46635');
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-46635');
await page.getByRole('button', {name: 'Create'}).click();
// Go back to list of runners.
await page.getByRole('link', {name: 'Runners', exact: true}).click();
// Edit the runner that was just created.
await page.getByRole('link', {name: 'Edit runner-46635'}).click();
await expect(page).toHaveTitle(/^Edit runner runner-46635 .*/);
await expect(page.getByRole('heading', {name: 'Edit runner runner-46635'})).toBeVisible();
// Make the form invalid to test validation.
await page.getByRole('textbox', {name: 'Name *'}).clear();
await page.getByRole('button', {name: 'Save'}).click();
await expect(page.locator('#flash-message')).toHaveText('Name cannot be empty.');
await expect(page.getByRole('textbox', {name: 'Name *'})).toBeEmpty();
await expect(page.getByRole('textbox', {name: 'Description'})).toHaveValue('Description of runner-46635');
// Submit a valid form.
await page.getByRole('textbox', {name: 'Name *'}).fill('runner-46636');
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-46636');
await page.getByRole('button', {name: 'Save'}).click();
// Verify that the runner's properties were updated properly.
await expect(page).toHaveTitle(/^Runner runner-46636 .*/);
await expect(page.locator('#flash-message')).toHaveText('Runner edited successfully');
await expect(page.getByRole('heading', {name: 'Runner runner-46636'})).toBeVisible();
await expect(page.getByLabel('Properties of runner-46636')).toMatchAriaSnapshot(`
- term: UUID
- definition: ${uuidPattern}
- term: Type
- definition: Individual
- term: Labels
- definition
- term: Last online time
- definition: Never
- term: Status
- definition: Offline
- term: Ephemeral
- definition: "no"
- term: Description
- definition: Description of runner-46636
`);
});
test('regenerate runner token', async ({page}) => {
await page.goto('/user/settings/actions/runners');
await page.getByRole('link', {name: 'Edit runner-2', exact: true}).click();
await expect(page).toHaveTitle(/^Edit runner runner-2 .*/);
await expect(page.getByRole('heading', {name: 'Edit runner runner-2'})).toBeVisible();
await page.getByRole('checkbox', {name: 'Regenerate token'}).check();
await page.getByRole('button', {name: 'Save'}).click();
// Verify set up instructions.
await expect(page).toHaveTitle(/^Set up runner runner-2 .*/);
await expect(page.getByRole('heading', {name: 'Set up runner runner-2'})).toBeVisible();
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
const runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerUUID).toEqual('3a20ad8d-d5d6-4b7b-ba55-841ac8264c17');
await page.getByRole('button', {name: 'Copy runner token'}).click();
const runnerToken = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerToken).not.toEqual('9730f9d2c6c731f07582788d1a1fe72a6b999a17');
expect(runnerToken).toMatch(tokenPattern);
await expect(page.getByRole('term')).toHaveText(['UUID', 'Token']);
await expect(page.getByRole('definition')).toContainText([runnerUUID, runnerToken]);
await expect(page.getByRole('heading', {name: 'Using the runner configuration file'})).toBeVisible();
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`uuid: ${runnerUUID}`);
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`token: ${runnerToken}`);
await expect(page.getByRole('heading', {name: 'Using program options'})).toBeVisible();
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--uuid ${runnerUUID}`);
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--token ${runnerToken}`);
});
test('delete runner', async ({page}) => {
await page.goto('/user/settings/actions/runners');
// We have to create a new runner because changes to fixtures affect the remainder of the tests in this file.
await page.getByRole('link', {name: 'Create new runner'}).click();
await page.getByRole('textbox', {name: 'Name *'}).fill('runner-660332');
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-660332');
await page.getByRole('button', {name: 'Create'}).click();
// Go back to list of runners.
await page.getByRole('link', {name: 'Runners', exact: true}).click();
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
await expect(page.getByRole('document')).toContainText('runner-660332');
// Delete the runner that was just created.
await page.getByRole('button', {name: 'Delete runner-660332'}).click();
// Confirm deletion
await expect(page.getByRole('document')).toContainText('Confirm to delete this runner');
await page.getByRole('button', {name: 'Yes', exact: true}).click();
// Verify that the runner is gone.
await expect(page.locator('#flash-message')).toHaveText('Runner deleted successfully');
await expect(page.getByRole('document')).not.toContainText('runner-660332');
});
});
test.describe('Global runners', () => {
test.use({user: 'user1'});
test('all runners are visible', async ({page}) => {
await page.goto('/admin/actions/runners');
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
const runnerContainer = page.locator('.runner-container');
const rows = runnerContainer.getByRole('row');
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
// the ordering of tests.
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
await expect(page.locator('tbody tr:has-text("8f940b0b-32a2-479a-9d48-06ab8d8a0b90")')).toMatchAriaSnapshot(`
- cell "runner-1 8f940b0b-32a2-479a-9d48-06ab8d8a0b90"
- cell "debian gpu"
- cell "Organization"
- cell "Offline"
- cell "Show details of runner-1"
- cell "Edit runner-1"
- cell "Delete runner-1"
`);
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toMatchAriaSnapshot(`
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
- cell "docker"
- cell "Individual"
- cell "Offline"
- cell "Show details of runner-2"
- cell "Edit runner-2"
- cell "Delete runner-2"
`);
await expect(page.locator('tbody tr:has-text("11c9a6da-0a92-46ea-a4f1-b6c98f8c781c")')).toMatchAriaSnapshot(`
- cell "runner-3 11c9a6da-0a92-46ea-a4f1-b6c98f8c781c"
- cell "fedora"
- cell "Organization"
- cell "Offline"
- cell "Show details of runner-3"
- cell "Edit runner-3"
- cell "Delete runner-3"
`);
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
- cell "docker"
- cell "Global"
- cell "Offline"
- cell "Show details of runner-4"
- cell "Edit runner-4"
- cell "Delete runner-4"
`);
await expect(page.locator('tbody tr:has-text("69d29449-1de5-4d17-845d-e3ae11a04a1b")')).toMatchAriaSnapshot(`
- cell "runner-5 69d29449-1de5-4d17-845d-e3ae11a04a1b"
- cell "debian"
- cell "Individual"
- cell "Offline"
- cell "Show details of runner-5"
- cell "Edit runner-5"
- cell "Delete runner-5"
`);
await expect(page.locator('tbody tr:has-text("9da25fbb-89a5-4520-a35a-d55fc94e4b76")')).toMatchAriaSnapshot(`
- cell "runner-6 9da25fbb-89a5-4520-a35a-d55fc94e4b76"
- cell "debian"
- cell "Repository"
- cell "Offline"
- cell "Show details of runner-6"
- cell "Edit runner-6"
- cell "Delete runner-6"
`);
await expect(page.locator('tbody tr:has-text("d935307e-1d2d-4b61-8885-bc8a1c52c269")')).toMatchAriaSnapshot(`
- cell "runner-7 d935307e-1d2d-4b61-8885-bc8a1c52c269"
- cell "alpine"
- cell "Individual"
- cell "Offline"
- cell "Show details of runner-7"
- cell "Edit runner-7"
- cell "Delete runner-7"
`);
});
test('runner details with all tasks visible on details page', async ({page}) => {
await page.goto('/admin/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
await expect(page.getByLabel('Properties of runner-4')).toMatchAriaSnapshot(`
- term: UUID
- definition: ${uuidPattern}
- term: Type
- definition: Global
- term: Labels
- definition: docker
- term: Last online time
- definition: Never
- term: Status
- definition: Offline
- term: Ephemeral
- definition: "no"
- term: Description
- definition: A runner for everyone
`);
await expect(page.getByRole('heading', {name: 'Recent tasks on this runner'})).toBeVisible();
const rows = page.getByRole('row');
// Only tasks from repositories owned by user2 should appear.
await expect(rows).toHaveCount(3);
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
await expect(rows.nth(1)).toHaveAccessibleName('88932 Waiting 49f55ab99b -');
await expect(rows.nth(2)).toHaveAccessibleName('88931 Running ed38c5a46c -');
});
test('create new runner', async ({page}) => {
await page.goto('/admin/actions/runners');
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
await page.getByRole('link', {name: 'Create new runner'}).click();
await expect(page).toHaveTitle(/^Create new runner .*/);
// Submit an invalid form to test validation.
await page.getByRole('button', {name: 'Create'}).click();
await expect(page.getByRole('paragraph')).toHaveText('Name cannot be empty.');
// Submit a valid form to create a runner.
await page.getByRole('textbox', {name: 'Name *'}).fill('runner-473465');
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-473465');
await page.getByRole('button', {name: 'Create'}).click();
// Verify set up instructions.
await expect(page).toHaveTitle(/^Set up runner runner-473465 .*/);
await expect(page.getByRole('heading', {name: 'Set up runner runner-473465'})).toBeVisible();
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
const runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerUUID).toMatch(uuidPattern);
await page.getByRole('button', {name: 'Copy runner token'}).click();
const runnerToken = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerToken).toMatch(tokenPattern);
await expect(page.getByRole('term')).toHaveText(['UUID', 'Token']);
await expect(page.getByRole('definition')).toContainText([runnerUUID, runnerToken]);
await expect(page.getByRole('heading', {name: 'Using the runner configuration file'})).toBeVisible();
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`uuid: ${runnerUUID}`);
await expect(page.getByLabel('Snippet to insert into the runner configuration')).toContainText(`token: ${runnerToken}`);
await expect(page.getByRole('heading', {name: 'Using program options'})).toBeVisible();
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--uuid ${runnerUUID}`);
await expect(page.getByLabel('How to invoke forgejo-runner')).toContainText(`--token ${runnerToken}`);
// Go back to list of runners.
await page.getByRole('link', {name: 'List of runners', exact: true}).click();
await expect(page.locator(`tbody tr:has-text("${runnerUUID}")`)).toMatchAriaSnapshot(`
- cell "runner-473465 ${runnerUUID}"
- cell ""
- cell "Global"
- cell "Offline"
- cell "Show details of runner-473465"
- cell "Edit runner-473465"
- cell "Delete runner-473465"
`);
});
test('edit runner without changing its token', async ({page}) => {
await page.goto('/admin/actions/runners');
// We have to create a new runner because changes to fixtures would affect the remainder of the tests in this file.
await page.getByRole('link', {name: 'Create new runner'}).click();
await page.getByRole('textbox', {name: 'Name *'}).fill('runner-956857');
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-956857');
await page.getByRole('button', {name: 'Create'}).click();
// Go back to list of runners.
await page.getByRole('link', {name: 'Runners', exact: true}).click();
// Edit the runner that was just created.
await page.getByRole('link', {name: 'Edit runner-956857'}).click();
await expect(page).toHaveTitle(/^Edit runner runner-956857 .*/);
await expect(page.getByRole('heading', {name: 'Edit runner runner-956857'})).toBeVisible();
// Make the form invalid to test validation.
await page.getByRole('textbox', {name: 'Name *'}).clear();
await page.getByRole('button', {name: 'Save'}).click();
await expect(page.locator('#flash-message')).toHaveText('Name cannot be empty.');
await expect(page.getByRole('textbox', {name: 'Name *'})).toBeEmpty();
await expect(page.getByRole('textbox', {name: 'Description'})).toHaveValue('Description of runner-956857');
// Submit a valid form.
await page.getByRole('textbox', {name: 'Name *'}).fill('runner-956858');
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-956858');
await page.getByRole('button', {name: 'Save'}).click();
// Verify that the runner's properties were updated properly.
await expect(page).toHaveTitle(/^Runner runner-956858 .*/);
await expect(page.locator('#flash-message')).toHaveText('Runner edited successfully');
await expect(page.getByRole('heading', {name: 'Runner runner-956858'})).toBeVisible();
await expect(page.getByLabel('Properties of runner-956858')).toMatchAriaSnapshot(`
- term: UUID
- definition: ${uuidPattern}
- term: Type
- definition: Global
- term: Labels
- definition
- term: Last online time
- definition: Never
- term: Status
- definition: Offline
- term: Ephemeral
- definition: "no"
- term: Description
- definition: Description of runner-956858
`);
});
test('delete runner', async ({page}) => {
await page.goto('/admin/actions/runners');
// We have to create a new runner because changes to fixtures affect the remainder of the tests in this file.
await page.getByRole('link', {name: 'Create new runner'}).click();
await page.getByRole('textbox', {name: 'Name *'}).fill('runner-650332');
await page.getByRole('textbox', {name: 'Description'}).fill('Description of runner-650332');
await page.getByRole('button', {name: 'Create'}).click();
// Go back to list of runners.
await page.getByRole('link', {name: 'Runners', exact: true}).click();
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
await expect(page.getByRole('document')).toContainText('runner-650332');
// Delete the runner that was just created.
await page.getByRole('button', {name: 'Delete runner-650332'}).click();
// Confirm deletion
await expect(page.getByRole('document')).toContainText('Confirm to delete this runner');
await page.getByRole('button', {name: 'Yes', exact: true}).click();
// Verify that the runner is gone.
await expect(page.locator('#flash-message')).toHaveText('Runner deleted successfully');
await expect(page.getByRole('document')).not.toContainText('runner-650332');
});
});
test.describe('Organization runners', () => {
test.use({user: 'user2'});
test('usable runners are visible', async ({page}) => {
await page.goto('/org/org3/settings/actions/runners');
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
const runnerContainer = page.locator('.runner-container');
const rows = runnerContainer.getByRole('row');
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
// the ordering of tests.
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
await expect(page.locator('tbody tr:has-text("8f940b0b-32a2-479a-9d48-06ab8d8a0b90")')).toMatchAriaSnapshot(`
- cell "runner-1 8f940b0b-32a2-479a-9d48-06ab8d8a0b90"
- cell "debian gpu"
- cell "Organization"
- cell "Offline"
- cell "Show details of runner-1"
- cell "Edit runner-1"
- cell "Delete runner-1"
`);
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
- cell "docker"
- cell "Global"
- cell "Offline"
- cell "Show details of runner-4"
- cell
- cell
`);
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toBeHidden();
await expect(page.locator('tbody tr:has-text("11c9a6da-0a92-46ea-a4f1-b6c98f8c781c")')).toBeHidden();
await expect(page.locator('tbody tr:has-text("69d29449-1de5-4d17-845d-e3ae11a04a1b")')).toBeHidden();
await expect(page.locator('tbody tr:has-text("9da25fbb-89a5-4520-a35a-d55fc94e4b76")')).toBeHidden();
await expect(page.locator('tbody tr:has-text("d935307e-1d2d-4b61-8885-bc8a1c52c269")')).toBeHidden();
// Verify that details of usable runners are accessible.
await page.getByRole('link', {name: 'Show details of runner-1', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-1 .*/);
await page.goto('/org/org3/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
});
test('runner details with tasks of repositories owned by organization', async ({page}) => {
await page.goto('/org/org3/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
await expect(page.getByLabel('Properties of runner-4')).toMatchAriaSnapshot(`
- term: UUID
- definition: ${uuidPattern}
- term: Type
- definition: Global
- term: Labels
- definition: docker
- term: Last online time
- definition: Never
- term: Status
- definition: Offline
- term: Ephemeral
- definition: "no"
- term: Description
- definition: A runner for everyone
`);
await expect(page.getByRole('heading', {name: 'Recent tasks on this runner within this organization'})).toBeVisible();
const rows = page.getByRole('row');
// Only tasks from repositories owned by org3 should appear.
await expect(rows).toHaveCount(2);
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
await expect(rows.nth(1)).toHaveAccessibleName('88931 Running ed38c5a46c -');
});
test('runner details with multiple pages of tasks', async ({page}) => {
await page.goto('/org/org3/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-1', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-1 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-1'})).toBeVisible();
await expect(page.getByRole('heading', {name: 'Recent tasks on this runner within this organization'})).toBeVisible();
const rows = page.getByRole('row');
await expect(rows).toHaveCount(31); // 30 runners plus table header
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
await expect(rows.nth(1)).toHaveAccessibleName('88930 Canceled ed4df76f86 -');
await expect(rows.nth(30)).toHaveAccessibleName('88900 Success aa06c3e960 -');
await page.getByRole('link', {name: 'Next', exact: true}).click();
await expect(rows).toHaveCount(2);
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
await expect(rows.nth(1)).toHaveAccessibleName('88899 Success d553d4419a -');
// Go back to the first page and verify that nothing has changed.
await page.getByRole('link', {name: 'Previous', exact: true}).click();
await expect(rows).toHaveCount(31); // 30 runners plus table header
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
await expect(rows.nth(1)).toHaveAccessibleName('88930 Canceled ed4df76f86 -');
await expect(rows.nth(30)).toHaveAccessibleName('88900 Success aa06c3e960 -');
});
});
test.describe('Repository runners', () => {
test.use({user: 'user2'});
test('usable runners are visible', async ({page}) => {
await page.goto('/user2/test_workflows/settings/actions/runners');
await expect(page.getByRole('heading', {name: 'Manage runners'})).toBeVisible();
const runnerContainer = page.locator('.runner-container');
const rows = runnerContainer.getByRole('row');
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
// the ordering of tests.
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toMatchAriaSnapshot(`
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
- cell "docker"
- cell "Individual"
- cell "Offline"
- cell "Show details of runner-2"
- cell
- cell
`);
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
- cell "docker"
- cell "Global"
- cell "Offline"
- cell "Show details of runner-4"
- cell
- cell
`);
await expect(page.locator('tbody tr:has-text("9da25fbb-89a5-4520-a35a-d55fc94e4b76")')).toMatchAriaSnapshot(`
- cell "runner-6 9da25fbb-89a5-4520-a35a-d55fc94e4b76"
- cell "debian"
- cell "Repository"
- cell "Offline"
- cell "Show details of runner-6"
- cell "Edit runner-6"
- cell "Delete runner-6"
`);
await expect(page.locator('tbody tr:has-text("11c9a6da-0a92-46ea-a4f1-b6c98f8c781c")')).toBeHidden();
await expect(page.locator('tbody tr:has-text("69d29449-1de5-4d17-845d-e3ae11a04a1b")')).toBeHidden();
await expect(page.locator('tbody tr:has-text("d935307e-1d2d-4b61-8885-bc8a1c52c269")')).toBeHidden();
// Verify that details of usable runners are accessible.
await page.getByRole('link', {name: 'Show details of runner-2', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-2 .*/);
await page.goto('/user2/test_workflows/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await page.goto('/user2/test_workflows/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-6', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-6 .*/);
});
test('runner details with tasks of repository only', async ({page}) => {
await page.goto('/user2/test_workflows/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
await expect(page.getByLabel('Properties of runner-4')).toMatchAriaSnapshot(`
- term: UUID
- definition: ${uuidPattern}
- term: Type
- definition: Global
- term: Labels
- definition: docker
- term: Last online time
- definition: Never
- term: Status
- definition: Offline
- term: Ephemeral
- definition: "no"
- term: Description
- definition: A runner for everyone
`);
await expect(page.getByRole('heading', {name: 'Recent tasks of this repository on this runner'})).toBeVisible();
const rows = page.getByRole('row');
// Only tasks from this repository should appear.
await expect(rows).toHaveCount(2);
await expect(rows.nth(0)).toHaveAccessibleName('Run Status Repository Commit Done at');
await expect(rows.nth(1)).toHaveAccessibleName('There are no tasks yet.');
});
});