forgejo/tests/e2e/issue-sidebar.test.e2e.ts
Gusted 0737196842 chore: remove webkit and mobile safari from playwright (#10103)
Webkit and Mobile safari are comically unreliable, will fail for unexplainable reasons and are very hard to run locally in comparison with the other supported platforms. I do not remember the last time where these two platforms were able to catch a regression where the other platforms did not.

I would like to stress, for the historical record, that many hours has been devoted into adjusting the tests and following best practices to make these two platforms more stable but despite those, IMO wasted, efforts these two platforms are causing many hours of wasted CPU time simply because they are flaky and make (new) contributors nervous if their change contains a regression or not.

To my knowledge, the tests are not broken for these two platforms. If you go to the issue tracker you will not find issues by users that use these two platforms and report that Forgejo is broken. It does not reflect reality.

This is the sunk cost fallacy, bite the bullet and agree that these platforms will not contribute positively to Forgejo's excellent test suite.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10103
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
2025-11-13 17:23:08 +01:00

358 lines
15 KiB
TypeScript

// @watch start
// templates/repo/issue/view_content/**
// web_src/css/repo/issue-**
// web_src/js/features/repo-issue**
// @watch end
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */
import {expect, type Page} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
test.describe('Pull: Toggle WIP', () => {
const prTitle = 'pull5';
async function toggle_wip_to({page}, should: boolean) {
await page.waitForLoadState('domcontentloaded');
if (should) {
await page.getByText('Still in progress?').click();
} else {
await page.getByText('Ready for review?').click();
}
}
async function check_wip({page}, is: boolean) {
const elemTitle = 'h1';
const stateLabel = '.issue-state-label';
await page.waitForLoadState('domcontentloaded');
await expect(page.locator(elemTitle)).toContainText(prTitle);
await expect(page.locator(elemTitle)).toContainText('#5');
if (is) {
await expect(page.locator(elemTitle)).toContainText('WIP');
await expect(page.locator(stateLabel)).toContainText('Draft');
} else {
await expect(page.locator(elemTitle)).not.toContainText('WIP');
await expect(page.locator(stateLabel)).toContainText('Open');
}
}
test.beforeEach(async ({page}) => {
const response = await page.goto('/user2/repo1/pulls/5');
expect(response?.status()).toBe(200); // Status OK
// ensure original title
await page.locator('#issue-title-edit-show').click();
await page.locator('#issue-title-editor input').fill(prTitle);
await page.getByText('Save').click();
await check_wip({page}, false);
});
test('simple toggle', async ({page}) => {
// toggle to WIP
await toggle_wip_to({page}, true);
await check_wip({page}, true);
// remove WIP
await toggle_wip_to({page}, false);
await check_wip({page}, false);
});
test('manual edit', async ({page}) => {
await page.goto('/user2/repo1/pulls/5');
// manually edit title to another prefix
await page.locator('#issue-title-edit-show').click();
await page.locator('#issue-title-editor input').fill(`[WIP] ${prTitle}`);
await page.getByText('Save').click();
await check_wip({page}, true);
// remove again
await toggle_wip_to({page}, false);
await check_wip({page}, false);
});
test('maximum title length', async ({page}) => {
await page.goto('/user2/repo1/pulls/5');
// check maximum title length is handled gracefully
const maxLenStr = prTitle + 'a'.repeat(240);
await page.locator('#issue-title-edit-show').click();
await page.locator('#issue-title-editor input').fill(maxLenStr);
await page.getByText('Save').click();
await expect(page.locator('h1')).toContainText(maxLenStr);
await check_wip({page}, false);
await toggle_wip_to({page}, true);
await check_wip({page}, true);
await expect(page.locator('h1')).toContainText(maxLenStr);
await toggle_wip_to({page}, false);
await check_wip({page}, false);
await expect(page.locator('h1')).toContainText(maxLenStr);
});
});
test('Issue: Labels', async ({page}) => {
async function submitLabels({page}: {page: Page}) {
const submitted = page.waitForResponse('/user2/repo1/issues/labels');
await page.locator('textarea').first().click(); // close via unrelated element
await submitted;
await page.waitForLoadState();
}
// select label list in sidebar only
const labelList = page.locator('.issue-content-right .labels-list a');
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
// restore initial state
await page.locator('.select-label').click();
const responsePromise = page.waitForResponse('/user2/repo1/issues/labels');
await page.getByText('Clear labels').click();
await responsePromise;
await expect(labelList.filter({hasText: 'label1'})).toBeHidden();
await expect(labelList.filter({hasText: 'label2'})).toBeHidden();
// add both labels
await page.locator('.select-label').click();
// label search could be tested this way:
// await page.locator('.select-label input').fill('label2');
await page.locator('.select-label .item').filter({hasText: 'label2'}).click();
await page.locator('.select-label .item').filter({hasText: 'label1'}).click();
await submitLabels({page});
await expect(labelList.filter({hasText: 'label2'})).toBeVisible();
await expect(labelList.filter({hasText: 'label1'})).toBeVisible();
// test removing label2 again
// due to a race condition, the page could still be "reloading",
// closing the dropdown after it was clicked.
// Retry the interaction as a group
// also see https://playwright.dev/docs/test-assertions#expecttopass
await expect(async () => {
await page.locator('.select-label').click();
await page.locator('.select-label .item').filter({hasText: 'label2'}).click();
}).toPass();
await submitLabels({page});
await expect(labelList.filter({hasText: 'label2'})).toBeHidden();
await expect(labelList.filter({hasText: 'label1'})).toBeVisible();
});
test('Issue: Assignees', async ({page}) => {
// select label list in sidebar only
const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item a');
const response = await page.goto('/org3/repo3/issues/1');
expect(response?.status()).toBe(200);
// Clear all assignees
await page.locator('.select-assignees-modify.dropdown').click();
await page.locator('.select-assignees-modify.dropdown .no-select.item').click();
await expect(assigneesList.filter({hasText: 'user2'})).toBeHidden();
await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeVisible();
await expect(page.locator('.select-assign-me')).toBeVisible();
// Assign other user (with searchbox)
await page.locator('.select-assignees-modify.dropdown').click();
await page.type('.select-assignees-modify .menu .search input', 'user4');
await expect(page.locator('.select-assignees-modify .menu .item').filter({hasText: 'user2'})).toBeHidden();
await expect(page.locator('.select-assignees-modify .menu .item').filter({hasText: 'user4'})).toBeVisible();
await page.locator('.select-assignees-modify .menu .item').filter({hasText: 'user4'}).click();
await page.locator('.select-assignees-modify.dropdown').click();
await expect(assigneesList.filter({hasText: 'user4'})).toBeVisible();
// remove user4
await page.locator('.select-assignees-modify.dropdown').click();
await page.locator('.select-assignees-modify .menu .item').filter({hasText: 'user4'}).click();
await page.locator('.select-assignees-modify.dropdown').click();
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeVisible();
await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
// Test assign me
await page.locator('.ui.assignees .select-assign-me').click();
await expect(assigneesList.filter({hasText: 'user2'})).toBeVisible();
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeHidden();
});
test('New Issue: Assignees', async ({page}) => {
// select label list in sidebar only
const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item');
const response = await page.goto('/org3/repo3/issues/new');
expect(response?.status()).toBe(200);
// preconditions
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeVisible();
await expect(assigneesList.filter({hasText: 'user2'})).toBeHidden();
await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
// Assign other user (with searchbox)
await page.locator('.select-assignees.dropdown').click();
await page.fill('.select-assignees .menu .search input', 'user4');
await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user2'})).toBeHidden();
await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user4'})).toBeVisible();
await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click();
await page.locator('.select-assignees.dropdown').click();
await expect(assigneesList.filter({hasText: 'user4'})).toBeVisible();
await screenshot(page, page.locator('.issue-content-right'));
// remove user4
await page.locator('.select-assignees.dropdown').click();
await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click();
await page.locator('.select-assignees.dropdown').click();
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeVisible();
await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
// Test assign me
await page.locator('.ui.assignees .select-assign-me').click();
await expect(assigneesList.filter({hasText: 'user2'})).toBeVisible();
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeHidden();
await page.locator('.select-assignees.dropdown').click();
await page.fill('.select-assignees .menu .search input', '');
await page.locator('.select-assignees.dropdown .no-select.item').click();
await expect(page.locator('.select-assign-me')).toBeVisible();
await screenshot(page, page.locator('div.filter.menu[data-id="#assignee_ids"]'), 30);
});
test('Issue: Milestone', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
const selectedMilestone = page.locator('.issue-content-right #selected-milestone');
const milestoneDropdown = page.locator('.issue-content-right .select-milestone.dropdown');
await expect(selectedMilestone).toContainText('No milestone');
// Add milestone.
await milestoneDropdown.click();
await page.getByRole('option', {name: 'milestone1'}).click();
await expect(selectedMilestone).toContainText('milestone1');
await expect(page.locator('.timeline-item.event').last()).toContainText('user2 added this to the milestone1 milestone');
// Clear milestone.
await milestoneDropdown.click();
await page.getByText('Clear milestone', {exact: true}).click();
await expect(selectedMilestone).toContainText('No milestone');
await expect(page.locator('.timeline-item.event').last()).toContainText('user2 removed this from the milestone1 milestone');
});
test('New Issue: Milestone', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
const selectedMilestone = page.locator('.issue-content-right .select-milestone.list');
const milestoneDropdown = page.locator('.issue-content-right .select-milestone.dropdown');
await expect(selectedMilestone).toContainText('No milestone');
await screenshot(page, page.locator('.issue-content-right'));
// Add milestone.
await milestoneDropdown.click();
await screenshot(page, page.locator('.menu.transition.visible'), 30);
await page.getByRole('option', {name: 'milestone1'}).click();
await expect(selectedMilestone).toContainText('milestone1');
await screenshot(page, page.locator('.issue-content-right'));
// Clear milestone.
await milestoneDropdown.click();
await page.getByText('Clear milestone', {exact: true}).click();
await expect(selectedMilestone).toContainText('No milestone');
await screenshot(page, page.locator('.issue-content-right'));
});
test.describe('Dependency dropdown', () => {
test.use({user: 'user11'});
test('Issue: Dependencies', async ({page}) => {
const response = await page.goto('/user11/dependency-test/issues/3');
expect(response?.status()).toBe(200);
const depsBlock = page.locator('.issue-content-right .depending');
const deleteDepBtn = page.locator('.issue-content-right .depending .delete-dependency-button');
const input = page.locator('#new-dependency-drop-list .search');
const current = page.locator('#new-dependency-drop-list .text').first();
const menu = page.locator('#new-dependency-drop-list .menu');
const items = page.locator('#new-dependency-drop-list .menu .item');
const confirmDelete = async () => {
const modal = page.locator('.modal.remove-dependency');
await expect(modal).toBeVisible();
await expect(modal).toContainText('This will remove the dependency from this issue');
await modal.locator('button.ok').click();
};
// A kludge to set the dropdown to the *wrong* value so it lets us select the correct one next.
const resetDropdown = async () => {
if (await current.textContent().then((s) => s.includes('#4'))) return;
await input.click();
await input.fill('unrelated');
await expect(items.first()).toContainText('unrelated');
await items.first().click();
await expect(current).toContainText('#4');
await input.click();
};
await expect(depsBlock).toBeVisible();
while (await deleteDepBtn.first().isVisible()) {
await deleteDepBtn.first().click(); // wipe added dependencies from any previously failed tests
await confirmDelete();
}
await expect(depsBlock).toContainText('No dependencies set');
await input.scrollIntoViewIfNeeded();
await input.click();
const first = 'first issue here';
const second = 'second issue here';
const newest = 'newest issue';
// Without query, it should show issues in the same repo, sorted by date, except current one.
await expect(menu).toBeVisible();
await expect(items).toHaveCount(4); // 5 issues in this repo, minus current one
await expect(items.first()).toContainText(newest);
await expect(items.last()).toContainText(first);
await resetDropdown();
// With query, it should search all repos, but show current repo issues first.
await input.fill('right');
await expect(items.first()).toContainText(second);
await expect.poll(() => items.count()).toBeGreaterThan(1); // there is an issue in user11/dependency-test-2 containing the word "right"
await resetDropdown();
// When entering an issue number, it should always show that one first, then all text matches.
await input.fill('1');
await expect(items.first()).toContainText(first);
await expect(items.nth(1)).toBeVisible();
await resetDropdown();
// Should behave the same with a prefix
await input.fill('#1');
await expect(items.first()).toContainText(first);
// Selecting an issue
await items.first().click();
await expect(current).toContainText(first);
// Add dependency
const link = page.locator('.issue-content-right .depending .dependency a.title');
await page.locator('.issue-content-right .depending button').click();
await expect(link).toHaveAttribute('href', '/user11/dependency-test/issues/1');
// Remove dependency
await expect(deleteDepBtn).toBeVisible();
await deleteDepBtn.click();
await confirmDelete();
await expect(depsBlock).toContainText('No dependencies set');
});
});
test('Issue: Reference', async ({page}) => {
let response = await page.goto('/user2/repo1/pulls/5');
expect(response?.status()).toBe(200);
await expect(page.locator('.ui.reference .truncate')).toContainText(
'user2/repo1!5',
);
response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
await expect(page.locator('.ui.reference .truncate')).toContainText(
'user2/repo1#1',
);
});