{{$notificationUnreadCount := call .NotificationUnreadCount}} -
+
-
- +
+ {{ctx.Locale.Tr "notification.subscriptions"}} {{if and (eq .Status 1)}} {{$.CsrfTokenHtml}}
-
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl index 300d117e2b..9e4685b849 100644 --- a/templates/user/notification/notification_subscriptions.tmpl +++ b/templates/user/notification/notification_subscriptions.tmpl @@ -10,7 +10,7 @@ {{ctx.Locale.Tr "notification.watching"}}
- + {{ctx.Locale.Tr "notifications"}}
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl index a5912c9e0f..602c538f97 100644 --- a/templates/user/settings/applications.tmpl +++ b/templates/user/settings/applications.tmpl @@ -59,7 +59,7 @@ {{.CsrfTokenHtml}} -
+
diff --git a/tests/e2e/actions.test.e2e.ts b/tests/e2e/actions.test.e2e.ts index e7fef6bc0a..083b25a7b9 100644 --- a/tests/e2e/actions.test.e2e.ts +++ b/tests/e2e/actions.test.e2e.ts @@ -10,7 +10,8 @@ // @watch end import {expect, type Page, type TestInfo} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; const workflow_trigger_notification_text = 'This workflow has a workflow_dispatch event trigger.'; @@ -21,13 +22,13 @@ async function dispatchSuccess(page: Page, testInfo: TestInfo) { await page.locator('#workflow_dispatch_dropdown>button').click(); await page.fill('input[name="inputs[string2]"]', 'abc'); - await save_visual(page); + await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'})); await page.locator('#workflow-dispatch-submit').click(); await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible(); await expect(page.locator('.run-list>:first-child .run-list-meta', {hasText: 'now'})).toBeVisible(); - await save_visual(page); + await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'})); } test.describe('Workflow Authenticated user2', () => { @@ -45,7 +46,7 @@ test.describe('Workflow Authenticated user2', () => { await expect(menu).toBeHidden(); await run_workflow_btn.click(); await expect(menu).toBeVisible(); - await save_visual(page); + await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'})); }); test('dispatch error: missing inputs', async ({page}, testInfo) => { @@ -64,7 +65,7 @@ test.describe('Workflow Authenticated user2', () => { await page.locator('#workflow-dispatch-submit').click(); await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible(); - await save_visual(page); + await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'})); }); // no assertions as the login in this test case is extracted for reuse @@ -78,7 +79,7 @@ test('workflow dispatch box not available for unauthenticated users', async ({pa await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); await expect(page.locator('body')).not.toContainText(workflow_trigger_notification_text); - await save_visual(page); + await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'})); }); async function completeDynamicRefresh(page: Page) { @@ -119,7 +120,7 @@ test.describe('workflow list dynamic refresh', () => { }); await completeDynamicRefresh(page); await expect(backgroundPage.locator('.run-list>:first-child .flex-item-body>b', {hasText: latestDispatchedRun})).toBeVisible(); - await save_visual(backgroundPage); + await screenshot(backgroundPage, page.locator('div.ui.container').filter({hasText: 'All workflows'})); }); test('refreshes on interval', async ({page}, testInfo) => { @@ -137,7 +138,7 @@ test.describe('workflow list dynamic refresh', () => { await simulatePollingInterval(backgroundPage); await expect(backgroundPage.locator('.run-list>:first-child .flex-item-body>b', {hasText: latestDispatchedRun})).toBeVisible(); - await save_visual(backgroundPage); + await screenshot(backgroundPage, page.locator('div.ui.container').filter({hasText: 'All workflows'})); }); test('post-refresh the dropdowns continue to operate', async ({page}, testInfo) => { diff --git a/tests/e2e/admin-ui.test.e2e.ts b/tests/e2e/admin-ui.test.e2e.ts new file mode 100644 index 0000000000..8fcd3456a3 --- /dev/null +++ b/tests/e2e/admin-ui.test.e2e.ts @@ -0,0 +1,58 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// web_src/js/features/admin/** +// templates/admin/** +// @watch end + +import {expect} from '@playwright/test'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; + +test.use({user: 'user1'}); + +test('Admin notices modal', async ({page}) => { + const response = await page.goto('/admin/notices'); + expect(response?.status()).toBe(200); + + await page.getByText('description1').click(); + await expect(page.locator('#detail-modal .content')).toHaveText('description1'); + await screenshot(page, page.locator('#detail-modal')); + await page.getByText('Cancel').click(); + await expect(page.locator('#change-email-modal')).toBeHidden(); + + await page.getByText('description2').click(); + await expect(page.locator('#detail-modal .content')).toHaveText('description2'); + await screenshot(page, page.locator('#detail-modal')); + await page.getByText('Cancel').click(); + await expect(page.locator('#change-email-modal')).toBeHidden(); + + await page.getByText('description3').click(); + await expect(page.locator('#detail-modal .content')).toHaveText('description3'); + await screenshot(page, page.locator('#detail-modal')); + await page.getByText('Cancel').click(); + await expect(page.locator('#change-email-modal')).toBeHidden(); +}); + +test('Admin email list', async ({page}) => { + const response = await page.goto('/admin/emails'); + expect(response?.status()).toBe(200); + + await page.locator('[data-uid="21"]').click(); + await expect(page.locator('#change-email-modal .content')).toHaveText('Are you sure you want to update this email address?'); + await screenshot(page, page.locator('#change-email-modal .content')); + await page.locator('#email-action-form').getByText('No').click(); + await expect(page.locator('#change-email-modal')).toBeHidden(); + + const activated = await page.locator('[data-uid="9"] .svg').evaluate((node) => node.classList.contains('octicon-check')); + await page.locator('[data-uid="9"]').click(); + await page.getByRole('button', {name: 'Yes'}).click(); + + // Retry-proof + if (activated) { + await expect(page.locator('[data-uid="9"] .svg')).toHaveClass(/octicon-x/); + } else { + await expect(page.locator('[data-uid="9"] svg')).toHaveClass(/octicon-check/); + } +}); diff --git a/tests/e2e/buttons.test.e2e.ts b/tests/e2e/buttons.test.e2e.ts new file mode 100644 index 0000000000..ea46d5dd23 --- /dev/null +++ b/tests/e2e/buttons.test.e2e.ts @@ -0,0 +1,34 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// web_src/css/modules/switch.css +// web_src/css/modules/button.css +// web_src/css/modules/dropdown.css +// @watch end + +import {expect} from '@playwright/test'; +import {test} from './utils_e2e.ts'; + +test.use({user: 'user2'}); + +test('Buttons and other controls have consistent height', async ({page}) => { + await page.goto('/user1'); + + // The height of dropdown opener and the button should be matching, even in mobile browsers with coarse pointer + let buttonHeight = (await page.locator('#profile-avatar-card .actions .primary-action').boundingBox()).height; + const openerHeight = (await page.locator('#profile-avatar-card .actions .dropdown').boundingBox()).height; + expect(openerHeight).toBe(buttonHeight); + + await page.goto('/notifications'); + + // The height should also be consistent with the button on the previous page + const switchHeight = (await page.locator('.switch').boundingBox()).height; + expect(buttonHeight).toBe(switchHeight); + + buttonHeight = (await page.locator('.button-row .button[href="/notifications/subscriptions"]').boundingBox()).height; + expect(buttonHeight).toBe(switchHeight); + + const purgeButtonHeight = (await page.locator('form[action="/notifications/purge"]').boundingBox()).height; + expect(buttonHeight).toBe(purgeButtonHeight); +}); diff --git a/tests/e2e/clipboard-copy.test.e2e.ts b/tests/e2e/clipboard-copy.test.e2e.ts index 2517d07463..2d159e8e90 100644 --- a/tests/e2e/clipboard-copy.test.e2e.ts +++ b/tests/e2e/clipboard-copy.test.e2e.ts @@ -8,7 +8,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test('copy src file path to clipboard', async ({page}, workerInfo) => { test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'Apple clipboard API addon - starting at just $499!'); @@ -20,7 +21,7 @@ test('copy src file path to clipboard', async ({page}, workerInfo) => { const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); expect(clipboardText).toContain('README.md'); await expect(page.getByText('Copied')).toBeVisible(); - await save_visual(page); + await screenshot(page, page.getByText('Copied'), 50); }); test('copy diff file path to clipboard', async ({page}, workerInfo) => { @@ -33,5 +34,5 @@ test('copy diff file path to clipboard', async ({page}, workerInfo) => { const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); expect(clipboardText).toContain('README.md'); await expect(page.getByText('Copied')).toBeVisible(); - await save_visual(page); + await screenshot(page, page.getByText('Copied'), 50); }); diff --git a/tests/e2e/dashboard-ci-status.test.e2e.ts b/tests/e2e/dashboard-ci-status.test.e2e.ts index d35fe299ff..06c8147770 100644 --- a/tests/e2e/dashboard-ci-status.test.e2e.ts +++ b/tests/e2e/dashboard-ci-status.test.e2e.ts @@ -24,5 +24,5 @@ test('Correct link and tooltip', async ({page}, testInfo) => { await expect(repoStatus).toHaveAttribute('href', '/user2/test_workflows/actions', {timeout: 10000}); await expect(repoStatus).toHaveAttribute('data-tooltip-content', /^(Error|Failure)$/); // ToDo: Ensure stable screenshot of dashboard. Known to be flaky: https://code.forgejo.org/forgejo/visual-browser-testing/commit/206d4cfb7a4af6d8d7043026cdd4d63708798b2a - // await save_visual(page); + // await screenshot(page); }); diff --git a/tests/e2e/declare_repos_test.go b/tests/e2e/declare_repos_test.go index 1aca84125a..8afad81df3 100644 --- a/tests/e2e/declare_repos_test.go +++ b/tests/e2e/declare_repos_test.go @@ -76,6 +76,10 @@ func DeclareGitRepos(t *testing.T) func() { CommitMsg: "Another commit which mentions @user1 in the title\nand @user2 in the text", }, }, nil), + newRepo(t, 2, "file-uploads", nil, []FileChanges{{ + Filename: "UPLOAD_TEST.md", + Versions: []string{"# File upload test\nUse this repo to test various file upload features in new branches."}, + }}, nil), newRepo(t, 2, "unicode-escaping", &tests.DeclarativeRepoOptions{ EnabledUnits: optional.Some([]unit_model.Type{unit_model.TypeCode, unit_model.TypeWiki}), }, []FileChanges{{ diff --git a/tests/e2e/dimmer.test.e2e.ts b/tests/e2e/dimmer.test.e2e.ts index ed8d116e1f..ffdbf7e20b 100644 --- a/tests/e2e/dimmer.test.e2e.ts +++ b/tests/e2e/dimmer.test.e2e.ts @@ -5,7 +5,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -23,13 +24,13 @@ test('Dimmed modal', async ({page}) => { // Modal and dimmer should be visible. await expect(page.locator('#block-user')).toBeVisible(); await expect(page.locator('.ui.dimmer')).toBeVisible(); - await save_visual(page); + await screenshot(page, page.locator('.ui.g-modal-confirm.delete.modal'), 50); // After canceling, modal and dimmer should be hidden. await page.locator('#block-user .cancel').click(); await expect(page.locator('.ui.dimmer')).toBeHidden(); await expect(page.locator('#block-user')).toBeHidden(); - await save_visual(page); + await screenshot(page); // Open the block modal and make the dimmer visible again. await page.locator('.actions .dropdown').click(); @@ -37,7 +38,7 @@ test('Dimmed modal', async ({page}) => { await expect(page.locator('#block-user')).toBeVisible(); await expect(page.locator('.ui.dimmer')).toBeVisible(); await expect(page.locator('.ui.dimmer')).toHaveCount(1); - await save_visual(page); + await screenshot(page, page.locator('.ui.g-modal-confirm.delete.modal'), 50); }); test('Dimmed overflow', async ({page}, workerInfo) => { @@ -58,7 +59,7 @@ test('Dimmed overflow', async ({page}, workerInfo) => { // Expect a 'are you sure, this file is empty' modal. await expect(page.locator('#edit-empty-content-modal')).toBeVisible(); await expect(page.locator('#edit-empty-content-modal header')).toContainText('Commit an empty file'); - await save_visual(page); + await screenshot(page); // Trickery to check the page cannot be scrolled. const {overflow} = await page.evaluate(() => { diff --git a/tests/e2e/dropdown.test.e2e.ts b/tests/e2e/dropdown.test.e2e.ts index 5f226f94bb..d6ab5c2294 100644 --- a/tests/e2e/dropdown.test.e2e.ts +++ b/tests/e2e/dropdown.test.e2e.ts @@ -9,7 +9,7 @@ import {expect} from '@playwright/test'; import {test} from './utils_e2e.ts'; -test('JS enhanced', async ({page}) => { +test('JS enhanced interaction', async ({page}) => { await page.goto('/user1'); await expect(page.locator('body')).not.toContainClass('no-js'); @@ -60,7 +60,7 @@ test('JS enhanced', async ({page}) => { await expect(languageMenu).toBeVisible(); }); -test('No JS', async ({browser}) => { +test('No JS interaction', async ({browser}) => { const context = await browser.newContext({javaScriptEnabled: false}); const nojsPage = await context.newPage(); await nojsPage.goto('/user1'); @@ -104,3 +104,55 @@ test('No JS', async ({browser}) => { await dropdownSummary.press(`Escape`); await expect(dropdownContent).toBeVisible(); }); + +test('Visual properties', async ({browser, isMobile}) => { + const context = await browser.newContext({javaScriptEnabled: false}); + const page = await context.newPage(); + + // User profile has dropdown used as an ellipsis menu + await page.goto('/user1'); + + // Has `.border` and pretty small default `inline-padding:` + const summary = page.locator('details.dropdown summary'); + expect(await summary.evaluate((el) => getComputedStyle(el).border)).toBe('1px solid rgba(0, 0, 0, 0.114)'); + expect(await summary.evaluate((el) => getComputedStyle(el).paddingInline)).toBe('7px'); + + // Background + expect(await summary.evaluate((el) => getComputedStyle(el).backgroundColor)).toBe('rgba(0, 0, 0, 0)'); + await summary.click(); + expect(await summary.evaluate((el) => getComputedStyle(el).backgroundColor)).toBe('rgb(226, 226, 229)'); + + // Direction and item height + const content = page.locator('details.dropdown > ul'); + const firstItem = page.locator('details.dropdown > ul > li:first-child'); + if (isMobile) { + // `
    `'s direction is reversed + expect(await content.evaluate((el) => getComputedStyle(el).direction)).toBe('rtl'); + expect(await firstItem.evaluate((el) => getComputedStyle(el).direction)).toBe('ltr'); + // `@media (pointer: coarse)` makes items taller + expect(await firstItem.evaluate((el) => getComputedStyle(el).height)).toBe('41px'); + } else { + // Both use default + expect(await content.evaluate((el) => getComputedStyle(el).direction)).toBe('ltr'); + expect(await firstItem.evaluate((el) => getComputedStyle(el).direction)).toBe('ltr'); + // Regular item height + expect(await firstItem.evaluate((el) => getComputedStyle(el).height)).toBe('34px'); + } + + // `/explore/users` has dropdown used as a sort options menu with text in the opener + await page.goto('/explore/users'); + + // No `.border` and increased `inline-padding:` from `.options` + expect(await summary.evaluate((el) => getComputedStyle(el).borderWidth)).toBe('0px'); + expect(await summary.evaluate((el) => getComputedStyle(el).paddingInline)).toBe('10.5px'); + + // `
      `'s direction is reversed + expect(await content.evaluate((el) => getComputedStyle(el).direction)).toBe('rtl'); + expect(await firstItem.evaluate((el) => getComputedStyle(el).direction)).toBe('ltr'); + + // Background of inactive and `.active` items + const activeItem = page.locator('details.dropdown > ul > li:first-child > a'); + const inactiveItem = page.locator('details.dropdown > ul > li:last-child > a'); + expect(await activeItem.evaluate((el) => getComputedStyle(el).backgroundColor)).toBe('rgb(226, 226, 229)'); + expect(await inactiveItem.evaluate((el) => getComputedStyle(el).backgroundColor)).toBe('rgba(0, 0, 0, 0)'); +}); diff --git a/tests/e2e/example.test.e2e.ts b/tests/e2e/example.test.e2e.ts index 97c5b8684b..8d94fa88d7 100644 --- a/tests/e2e/example.test.e2e.ts +++ b/tests/e2e/example.test.e2e.ts @@ -5,7 +5,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test('Load Homepage', async ({page}) => { const response = await page.goto('/'); @@ -26,7 +27,7 @@ test('Register Form', async ({page}, workerInfo) => { expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible(); await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!'); - await save_visual(page); + await screenshot(page); }); // eslint-disable-next-line playwright/no-skipped-test diff --git a/tests/e2e/explore.test.e2e.ts b/tests/e2e/explore.test.e2e.ts index 1bb5af3cc6..f953ba2d71 100644 --- a/tests/e2e/explore.test.e2e.ts +++ b/tests/e2e/explore.test.e2e.ts @@ -7,7 +7,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test('Explore view taborder', async ({page}) => { await page.goto('/explore/repos'); @@ -42,5 +43,5 @@ test('Explore view taborder', async ({page}) => { } } expect(res).toBe(exp); - await save_visual(page); + await screenshot(page); }); diff --git a/tests/e2e/fixtures/repo_unit.yml b/tests/e2e/fixtures/repo_unit.yml new file mode 100644 index 0000000000..df727bbc08 --- /dev/null +++ b/tests/e2e/fixtures/repo_unit.yml @@ -0,0 +1,6 @@ +- + id: 1001 + repo_id: 1002 + type: 1 + config: "{}" + created_unix: 946684810 diff --git a/tests/e2e/fixtures/repository.yml b/tests/e2e/fixtures/repository.yml index 6afbd138e8..153e6e7752 100644 --- a/tests/e2e/fixtures/repository.yml +++ b/tests/e2e/fixtures/repository.yml @@ -11,3 +11,18 @@ status: 0 lfs_size: 8192 topics: '[]' + + +- + id: 1002 + owner_id: 2 + owner_name: user2 + lower_name: rendering-test + name: rendering-test + default_branch: master + is_empty: false + is_archived: false + is_private: false + status: 0 + num_issues: 0 + topics: '[]' diff --git a/tests/e2e/git-notes.test.e2e.ts b/tests/e2e/git-notes.test.e2e.ts index 1e2cbe76fc..f1f4d07c1d 100644 --- a/tests/e2e/git-notes.test.e2e.ts +++ b/tests/e2e/git-notes.test.e2e.ts @@ -1,6 +1,7 @@ // @ts-check import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -16,15 +17,15 @@ test('Change git note', async ({page}) => { let textarea = page.locator('textarea[name="notes"]'); await expect(textarea).toBeVisible(); await textarea.fill('This is a new note'); - await save_visual(page); + await screenshot(page, page.locator('.ui.container.fluid.padded')); await page.locator('#notes-save-button').click(); - await save_visual(page); + await screenshot(page, page.locator('.ui.container.fluid.padded')); response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d'); expect(response?.status()).toBe(200); textarea = page.locator('textarea[name="notes"]'); await expect(textarea).toHaveText('This is a new note'); - await save_visual(page); + await screenshot(page, page.locator('.ui.container.fluid.padded')); }); diff --git a/tests/e2e/image-diff.test.e2e.ts b/tests/e2e/image-diff.test.e2e.ts index f7d4f7bd69..273564fcb3 100644 --- a/tests/e2e/image-diff.test.e2e.ts +++ b/tests/e2e/image-diff.test.e2e.ts @@ -6,7 +6,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test, dynamic_id} from './utils_e2e.ts'; +import {test, dynamic_id} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -44,7 +45,7 @@ test('Repository image diff', async ({page}) => { await expect(page.locator('.tab[data-tab="diff-side-by-side-1"]')).toBeVisible(); await expect(page.locator('.tab[data-tab="diff-swipe-1"]')).toBeHidden(); await expect(page.locator('.tab[data-tab="diff-overlay-1"]')).toBeHidden(); - await save_visual(page); + await screenshot(page, page.locator('#diff-container')); await page.getByText('Swipe').click(); await expect(page.locator('.item[data-tab="diff-side-by-side-1"]')).not.toContainClass('active'); @@ -53,7 +54,7 @@ test('Repository image diff', async ({page}) => { await expect(page.locator('.tab[data-tab="diff-side-by-side-1"]')).toBeHidden(); await expect(page.locator('.tab[data-tab="diff-swipe-1"]')).toBeVisible(); await expect(page.locator('.tab[data-tab="diff-overlay-1"]')).toBeHidden(); - await save_visual(page); + await screenshot(page, page.locator('#diff-container')); await page.getByText('Overlay').click(); await expect(page.locator('.item[data-tab="diff-side-by-side-1"]')).not.toContainClass('active'); @@ -62,5 +63,5 @@ test('Repository image diff', async ({page}) => { await expect(page.locator('.tab[data-tab="diff-side-by-side-1"]')).toBeHidden(); await expect(page.locator('.tab[data-tab="diff-swipe-1"]')).toBeHidden(); await expect(page.locator('.tab[data-tab="diff-overlay-1"]')).toBeVisible(); - await save_visual(page); + await screenshot(page, page.locator('#diff-container')); }); diff --git a/tests/e2e/issue-comment-dropzone.test.e2e.ts b/tests/e2e/issue-comment-dropzone.test.e2e.ts index 33ea2c9403..10fd27e714 100644 --- a/tests/e2e/issue-comment-dropzone.test.e2e.ts +++ b/tests/e2e/issue-comment-dropzone.test.e2e.ts @@ -9,7 +9,8 @@ // @watch end import {expect, type Locator, type Page, type TestInfo} from '@playwright/test'; -import {test, save_visual, dynamic_id} from './utils_e2e.ts'; +import {test, dynamic_id} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -57,7 +58,11 @@ async function assertCopy(page: Page, workerInfo: TestInfo, startWith: string) { test('Paste image in new comment', async ({page}, workerInfo) => { await page.goto('/user2/repo1/issues/new'); + const waitForAttachmentUpload = page.waitForResponse((response) => { + return response.request().method() === 'POST' && response.url().endsWith('/attachments'); + }); await pasteImage(page.locator('.markdown-text-editor')); + await waitForAttachmentUpload; const dropzone = page.locator('.dropzone'); await expect(dropzone.locator('.files')).toHaveCount(1); @@ -67,7 +72,7 @@ test('Paste image in new comment', async ({page}, workerInfo) => { await expect(preview.locator('.octicon-copy')).toBeVisible(); await assertCopy(page, workerInfo, '![foo]('); - await save_visual(page); + await screenshot(page, page.locator('.issue-content-left')); }); test('Re-add images to dropzone on edit', async ({page}, workerInfo) => { @@ -75,12 +80,20 @@ test('Re-add images to dropzone on edit', async ({page}, workerInfo) => { const issueTitle = dynamic_id(); await page.locator('#issue_title').fill(issueTitle); + const waitForAttachmentUpload = page.waitForResponse((response) => { + return response.request().method() === 'POST' && response.url().endsWith('/attachments'); + }); await pasteImage(page.locator('.markdown-text-editor')); + await waitForAttachmentUpload; await page.getByRole('button', {name: 'Create issue'}).click(); await expect(page).toHaveURL(/\/user2\/repo1\/issues\/\d+$/); await page.click('.comment-container .context-menu'); + const waitForAttachmentsLoad = page.waitForResponse((response) => { + return response.request().method() === 'GET' && response.url().endsWith('/attachments'); + }); await page.click('.comment-container .menu > .edit-content'); + await waitForAttachmentsLoad; const dropzone = page.locator('.dropzone'); await expect(dropzone.locator('.files').first()).toHaveCount(1); @@ -90,5 +103,5 @@ test('Re-add images to dropzone on edit', async ({page}, workerInfo) => { await expect(preview.locator('.octicon-copy')).toBeVisible(); await assertCopy(page, workerInfo, '![foo]('); - await save_visual(page); + await screenshot(page, page.locator('.issue-content-left')); }); diff --git a/tests/e2e/issue-comment.test.e2e.ts b/tests/e2e/issue-comment.test.e2e.ts index bc2bc3d691..ff2a1928b3 100644 --- a/tests/e2e/issue-comment.test.e2e.ts +++ b/tests/e2e/issue-comment.test.e2e.ts @@ -2,13 +2,66 @@ // web_src/js/features/comp/** // web_src/js/features/repo-** // templates/repo/issue/view_content/* +// routers/web/repo/issue_content_history.go // @watch end import {expect} from '@playwright/test'; -import {test, save_visual} from './utils_e2e.ts'; +import {test, dynamic_id, login_user} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); +for (const run of [ + {title: 'JS off', js: true}, + {title: 'JS on', js: false}, +]) { + test.describe(`Create issue & comment`, () => { + // playwright/valid-title says: [error] Title must be a string + test(`${run.title}`, async ({browser}, workerInfo) => { + test.skip(['Mobile Chrome', 'Mobile Safari'].includes(workerInfo.project.name), 'Mobile Chrome has trouble clicking Comment button with JS enabled, Mobile Safari is flaky and only passes on retry'); + + const issueTitle = dynamic_id(); + const issueContent = dynamic_id(); + const commentContent = dynamic_id(); + + const context = await login_user(browser, workerInfo, 'user2', {javaScriptEnabled: run.js}); + const page = await context.newPage(); + + let response = await page.goto('/user2/repo1/issues/new'); + expect(response?.status()).toBe(200); + + // Create a new issue + await page.getByPlaceholder('Title').fill(issueTitle); + await page.getByPlaceholder('Leave a comment').fill(issueContent); + await page.getByRole('button', {name: 'Create issue'}).click(); + + if (run.js) { + await expect(page).toHaveURL(/\/user2\/repo1\/issues\/\d+$/); + } else { + // NoJS clients end up on a .../comments JSON file and browsers surround it with some HTML + const redirectUrl = await JSON.parse(await page.locator('body').textContent())['redirect']; + response = await page.goto(redirectUrl); + expect(response?.status()).toBe(200); + } + + // Leave a comment + await page.locator('#comment-form').getByPlaceholder('Leave a comment').fill(commentContent); + await page.locator('#comment-form button.primary').filter({hasText: 'Comment'}).click(); + + if (!run.js) { + const redirectUrl = await JSON.parse(await page.locator('body').textContent())['redirect']; + response = await page.goto(redirectUrl); + expect(response?.status()).toBe(200); + } + + // Validate the page contents that actions above made a difference + await expect(page.locator('h1')).toContainText(issueTitle); + await expect(page.locator('.comment').filter({hasText: issueContent})).toHaveCount(1); + await expect(page.locator('.comment').filter({hasText: commentContent})).toHaveCount(1); + }); + }); +} + test('Menu accessibility', async ({page}) => { await page.goto('/user2/repo1/issues/1'); await expect(page.getByLabel('user2 reacted eyes. Remove eyes')).toBeVisible(); @@ -61,7 +114,7 @@ test('Always focus edit tab first on edit', async ({page}) => { // Switch to preview tab and save await page.click('#issue-1 .comment-container .context-menu'); await page.click('#issue-1 .comment-container .menu>.edit-content'); - await page.locator('#issue-1 .comment-container a[data-tab-for=markdown-previewer]').click(); + await page.locator('#issue-1 .comment-container [data-tab-for=markdown-previewer]').click(); await page.click('#issue-1 .comment-container .save'); await page.waitForLoadState(); @@ -69,12 +122,12 @@ test('Always focus edit tab first on edit', async ({page}) => { // Edit again and assert that edit tab should be active (and not preview tab) await page.click('#issue-1 .comment-container .context-menu'); await page.click('#issue-1 .comment-container .menu>.edit-content'); - const editTab = page.locator('#issue-1 .comment-container a[data-tab-for=markdown-writer]'); - const previewTab = page.locator('#issue-1 .comment-container a[data-tab-for=markdown-previewer]'); + const editTab = page.locator('#issue-1 .comment-container [data-tab-for=markdown-writer]'); + const previewTab = page.locator('#issue-1 .comment-container [data-tab-for=markdown-previewer]'); await expect(editTab).toHaveClass(/active/); await expect(previewTab).not.toHaveClass(/active/); - await save_visual(page); + await screenshot(page, page.locator('.issue-content-left')); }); test('Reset content of comment edit field on cancel', async ({page}) => { @@ -95,7 +148,7 @@ test('Reset content of comment edit field on cancel', async ({page}) => { await page.click('#issue-1 .comment-container .context-menu'); await page.click('#issue-1 .comment-container .menu>.edit-content'); await expect(editorTextarea).toHaveValue('content for the first issue'); - await save_visual(page); + await screenshot(page, page.locator('.issue-content-left')); }); test('Quote reply', async ({page}, workerInfo) => { @@ -236,3 +289,34 @@ test('Emoji suggestions', async ({page}) => { const item = suggestionList.locator(`li:has-text("forgejo")`); await expect(item.locator('img')).toHaveAttribute('src', '/assets/img/emoji/forgejo.png'); }); + +test('Comment history', async ({page}) => { + const response = await page.goto('/user2/repo1/issues/new'); + expect(response?.status()).toBe(200); + + // Create a new issue. + await page.getByPlaceholder('Title').fill('Just a title'); + await page.getByPlaceholder('Leave a comment').fill('Hi, have you considered using a rotating fish as logo?'); + await page.getByRole('button', {name: 'Create issue'}).click(); + await expect(page).toHaveURL(/\/user2\/repo1\/issues\/\d+$/); + + page.on('dialog', (dialog) => dialog.accept()); + + // Make a change. + const editorTextarea = page.locator('[id="_combo_markdown_editor_1"]'); + await page.click('.comment-container .context-menu'); + await page.click('.comment-container .menu>.edit-content'); + await editorTextarea.fill(dynamic_id()); + await page.click('.comment-container .edit .save'); + + // Reload the page so the edited bit is rendered. + await page.reload(); + + await page.getByText('• edited').click(); + await page.click('.content-history-menu .item:nth-child(1)'); + await page.getByText('Options').click(); + await page.getByText('Delete from history').click(); + + await page.getByText('• edited').click(); + await expect(page.locator(".content-history-menu .item s span[data-history-is-deleted='1']")).toBeVisible(); +}); diff --git a/tests/e2e/issue-sidebar.test.e2e.ts b/tests/e2e/issue-sidebar.test.e2e.ts index f181185d69..f8f0fadbd5 100644 --- a/tests/e2e/issue-sidebar.test.e2e.ts +++ b/tests/e2e/issue-sidebar.test.e2e.ts @@ -7,7 +7,8 @@ /* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */ import {expect, type Page} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -194,7 +195,7 @@ test('New Issue: Assignees', async ({page}, workerInfo) => { 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 save_visual(page); + await screenshot(page, page.locator('.issue-content-right')); // remove user4 await page.locator('.select-assignees.dropdown').click(); @@ -212,7 +213,7 @@ test('New Issue: Assignees', async ({page}, workerInfo) => { 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 save_visual(page); + await screenshot(page, page.locator('div.filter.menu[data-id="#assignee_ids"]'), 30); }); test('Issue: Milestone', async ({page}, workerInfo) => { @@ -247,19 +248,20 @@ test('New Issue: Milestone', async ({page}, workerInfo) => { 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 save_visual(page); + 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 save_visual(page); + 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 save_visual(page); + await screenshot(page, page.locator('.issue-content-right')); }); test.describe('Dependency dropdown', () => { diff --git a/tests/e2e/issue-timetracking.test.e2e.ts b/tests/e2e/issue-timetracking.test.e2e.ts index 901cbe793f..1cb65190bf 100644 --- a/tests/e2e/issue-timetracking.test.e2e.ts +++ b/tests/e2e/issue-timetracking.test.e2e.ts @@ -5,7 +5,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -31,7 +32,7 @@ test('Issue timetracking', async ({page}) => { // Verify it is shown in the issue sidebar await expect(page.locator('.issue-content-right .comments')).toContainText('Total time spent: 5 hours 32 minutes'); - await save_visual(page); + await screenshot(page); // Delete the added time. await page.getByRole('button', {name: 'Delete this time log'}).click(); diff --git a/tests/e2e/login.test.e2e.ts b/tests/e2e/login.test.e2e.ts index 01cf4d7b8d..465b9577d4 100644 --- a/tests/e2e/login.test.e2e.ts +++ b/tests/e2e/login.test.e2e.ts @@ -8,7 +8,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, test_context} from './utils_e2e.ts'; +import {test, test_context} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test('Mismatched ROOT_URL', async ({browser}, workerInfo) => { test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'init script gets randomly ignored'); @@ -27,7 +28,7 @@ test('Mismatched ROOT_URL', async ({browser}, workerInfo) => { const response = await page.goto('/user/login'); expect(response?.status()).toBe(200); - await save_visual(page); + await screenshot(page); const globalError = page.locator('.js-global-error'); await expect(globalError).toContainText('This Forgejo instance is configured to be served on '); await expect(globalError).toContainText('You are currently viewing Forgejo through a different URL, which may cause parts of the application to break. The canonical URL is controlled by Forgejo admins via the ROOT_URL setting in the app.ini.'); diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts index c2d4057bc9..132fa23988 100644 --- a/tests/e2e/markdown-editor.test.e2e.ts +++ b/tests/e2e/markdown-editor.test.e2e.ts @@ -8,7 +8,8 @@ import {expect} from '@playwright/test'; import {accessibilityCheck} from './shared/accessibility.ts'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -38,7 +39,7 @@ test('Markdown image preview behaviour', async ({page}, workerInfo) => { // Check for the image preview via the expected attribute const preview = page.locator('div[data-tab="preview"] p[dir="auto"] a'); await expect(preview).toHaveAttribute('href', 'http://localhost:3003/user2/repo1/media/branch/master/assets/logo.svg'); - await save_visual(page); + await screenshot(page); }); test('Markdown indentation via toolbar', async ({page}) => { @@ -196,6 +197,13 @@ test('markdown indentation with Tab', async ({page}) => { await textarea.pressSequentially(' '); await textarea.press('Shift+Tab'); await expect(textarea).toHaveValue(initText); + + // Check that indentation tokens not at the start of the string do not interrupt indentation + await textarea.focus(); + await textarea.fill(initText); + await textarea.pressSequentially(tab); + await textarea.press('Tab'); + await expect(textarea).toHaveValue(`* first\n* second\n* third\n * last `); }); test('markdown block quote indentation', async ({page}) => { @@ -352,7 +360,7 @@ test('Markdown insert table', async ({page}) => { const newTableModal = page.locator('div[data-markdown-table-modal-id="0"]'); await expect(newTableModal).toBeVisible(); - await save_visual(page); + await screenshot(page); await newTableModal.locator('input[name="table-rows"]').fill('3'); await newTableModal.locator('input[name="table-columns"]').fill('2'); @@ -363,10 +371,12 @@ test('Markdown insert table', async ({page}) => { const textarea = page.locator('textarea[name=content]'); await expect(textarea).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n'); - await save_visual(page); + await screenshot(page); }); -test('Markdown insert link', async ({page}) => { +test('Markdown insert link', async ({page}, workerInfo) => { + test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'Unreliable in this test'); + const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); @@ -376,7 +386,7 @@ test('Markdown insert link', async ({page}) => { const newLinkModal = page.locator('div[data-markdown-link-modal-id="0"]'); await expect(newLinkModal).toBeVisible(); await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"]'], [], []); - await save_visual(page); + await screenshot(page); const url = 'https://example.com'; const description = 'Where does this lead?'; @@ -390,7 +400,7 @@ test('Markdown insert link', async ({page}) => { const textarea = page.locator('textarea[name=content]'); await expect(textarea).toHaveValue(`[${description}](${url})`); - await save_visual(page); + await screenshot(page); }); test('text expander has higher prio then prefix continuation', async ({page}) => { @@ -438,25 +448,29 @@ test('Combo Markdown: preview mode switch', async ({page}) => { await textarea.fill('**Content** :100: _100_'); // Switch to preview mode - await page.locator('a[data-tab-for="markdown-previewer"]').click(); + await page.locator('[data-tab-for="markdown-previewer"]').click(); // Verify that the related UI elements were switched correctly await expect(toolbarItem).toBeHidden(); await expect(editorPanel).toBeHidden(); await expect(previewPanel).toBeVisible(); - await save_visual(page); + await screenshot(page); // Verify that some content rendered await expect(page.locator('[data-tab-panel="markdown-previewer"] .emoji[data-alias="100"]')).toBeVisible(); // Switch back to edit mode - await page.locator('a[data-tab-for="markdown-writer"]').click(); + await page.locator('[data-tab-for="markdown-writer"]').click(); // Verify that the related UI elements were switched back correctly await expect(toolbarItem).toBeVisible(); await expect(editorPanel).toBeVisible(); await expect(previewPanel).toBeHidden(); - await save_visual(page); + + // Validate switch height: it is customized to be same height as other buttons on the panel + expect(await page.locator('markdown-toolbar .switch').evaluate((el) => getComputedStyle(el).height)).toBe(await page.locator('md-header.markdown-toolbar-button').evaluate((el) => getComputedStyle(el).height)); + + await screenshot(page); }); test('Multiple combo markdown: insert table', async ({page}) => { @@ -493,7 +507,7 @@ test('Multiple combo markdown: insert table', async ({page}) => { await expect(comboboxOne).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n'); await expect(comboboxTwo).toBeEmpty(); - await save_visual(page); + await screenshot(page); // focus second one and add table to it await textareaTwo.click(); @@ -515,7 +529,7 @@ test('Multiple combo markdown: insert table', async ({page}) => { await expect(comboboxOne).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n'); await expect(comboboxTwo).toHaveValue('| Header | Header | Header |\n|---------|---------|---------|\n| Content | Content | Content |\n| Content | Content | Content |\n'); - await save_visual(page); + await screenshot(page); }); test('Markdown bold/italic toolbar and shortcut', async ({page}) => { diff --git a/tests/e2e/markup.test.e2e.ts b/tests/e2e/markup.test.e2e.ts index b26e83661b..a5e859f677 100644 --- a/tests/e2e/markup.test.e2e.ts +++ b/tests/e2e/markup.test.e2e.ts @@ -3,7 +3,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test('markup with #xyz-mode-only', async ({page}, workerInfo) => { test.skip(['webkit', 'Mobile Safari'].includes(workerInfo.project.name), 'Newest version contains a regression'); @@ -14,5 +15,5 @@ test('markup with #xyz-mode-only', async ({page}, workerInfo) => { await expect(comment).toBeVisible(); await expect(comment.locator('[src$="#gh-light-mode-only"]')).toBeVisible(); await expect(comment.locator('[src$="#gh-dark-mode-only"]')).toBeHidden(); - await save_visual(page); + await screenshot(page); }); diff --git a/tests/e2e/modal.test.e2e.ts b/tests/e2e/modal.test.e2e.ts index dbcfcb3ea4..946a7f6d46 100644 --- a/tests/e2e/modal.test.e2e.ts +++ b/tests/e2e/modal.test.e2e.ts @@ -7,7 +7,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, dynamic_id, test} from './utils_e2e.ts'; +import {dynamic_id, test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -33,7 +34,7 @@ test('Dialog modal', async ({page}, workerInfo) => { await page.keyboard.press('Backspace'); await page.locator('#commit-button').click(); - await save_visual(page); + await screenshot(page); await expect(page.locator('#edit-empty-content-modal')).toBeVisible(); await page.locator('#edit-empty-content-modal .cancel').click(); diff --git a/tests/e2e/org-settings.test.e2e.ts b/tests/e2e/org-settings.test.e2e.ts index df554e0674..b604ef0e8c 100644 --- a/tests/e2e/org-settings.test.e2e.ts +++ b/tests/e2e/org-settings.test.e2e.ts @@ -5,23 +5,25 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; import {validate_form} from './shared/forms.ts'; test.use({user: 'user2'}); test('org team settings', async ({page}, workerInfo) => { - test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual'); + test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'Unreliable in this test'); + const response = await page.goto('/org/org3/teams/team1/edit'); expect(response?.status()).toBe(200); await page.locator('input[name="permission"][value="admin"]').click(); await expect(page.locator('.hide-unless-checked')).toBeHidden(); - await save_visual(page); + await screenshot(page); await page.locator('input[name="permission"][value="read"]').click(); await expect(page.locator('.hide-unless-checked')).toBeVisible(); - await save_visual(page); + await screenshot(page); // we are validating the form here to include the part that could be hidden await validate_form({page}); diff --git a/tests/e2e/pr-review.test.e2e.ts b/tests/e2e/pr-review.test.e2e.ts index 4e95e0aa69..22f3971202 100644 --- a/tests/e2e/pr-review.test.e2e.ts +++ b/tests/e2e/pr-review.test.e2e.ts @@ -7,7 +7,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -16,18 +17,18 @@ test('PR: Create review from files', async ({page}) => { expect(response?.status()).toBe(200); await expect(page.locator('.tippy-box .review-box-panel')).toBeHidden(); - await save_visual(page); + await screenshot(page); // Review panel should appear after clicking Finish review await page.locator('#review-box .js-btn-review').click(); await expect(page.locator('.tippy-box .review-box-panel')).toBeVisible(); - await save_visual(page); + await screenshot(page); await page.locator('.review-box-panel textarea#_combo_markdown_editor_0') .fill('This is a review'); await page.locator('.review-box-panel button.btn-submit[value="approve"]').click(); await page.waitForURL(/.*\/user2\/repo1\/pulls\/5#issuecomment-\d+/); - await save_visual(page); + await screenshot(page); }); test('PR: Create review from commit', async ({page}) => { @@ -39,7 +40,7 @@ test('PR: Create review from commit', async ({page}) => { await expect(code_comment).toBeVisible(); await code_comment.fill('This is a code comment'); - await save_visual(page); + await screenshot(page); const start_button = page.locator('.comment-code-cloud form button.btn-start-review'); // Workaround for #7152, where there might already be a pending review state from previous @@ -58,13 +59,13 @@ test('PR: Create review from commit', async ({page}) => { await page.locator('#review-box .js-btn-review').click(); await expect(page.locator('.tippy-box .review-box-panel')).toBeVisible(); - await save_visual(page); + await screenshot(page); await page.locator('.review-box-panel textarea.markdown-text-editor') .fill('This is a review'); await page.locator('.review-box-panel button.btn-submit[value="approve"]').click(); await page.waitForURL(/.*\/user2\/repo1\/pulls\/3#issuecomment-\d+/); - await save_visual(page); + await screenshot(page); // In addition to testing the ability to delete comments, this also // performs clean up. If tests are run for multiple platforms, the data isn't reset @@ -79,7 +80,7 @@ test('PR: Create review from commit', async ({page}) => { await page.locator('.comment-header-right.actions div.menu .delete-comment').click(); await expect(page.locator('.comment-list .comment-container')).toBeHidden(); - await save_visual(page); + await screenshot(page); }); test('PR: Navigate by single commit', async ({page}) => { @@ -88,7 +89,7 @@ test('PR: Navigate by single commit', async ({page}) => { await page.locator('tbody.commit-list td.message a').nth(1).click(); await page.waitForURL(/.*\/user2\/repo1\/pulls\/3\/commits\/4a357436d925b5c974181ff12a994538ddc5a269/); - await save_visual(page); + await screenshot(page); let prevButton = page.locator('.commit-header-buttons').getByText(/Prev/); let nextButton = page.locator('.commit-header-buttons').getByText(/Next/); @@ -101,7 +102,7 @@ test('PR: Navigate by single commit', async ({page}) => { await nextButton.click(); await page.waitForURL(/.*\/user2\/repo1\/pulls\/3\/commits\/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd/); - await save_visual(page); + await screenshot(page); prevButton = page.locator('.commit-header-buttons').getByText(/Prev/); nextButton = page.locator('.commit-header-buttons').getByText(/Next/); @@ -122,7 +123,7 @@ test('PR: Test mentions values', async ({page}) => { await page.locator('.review-box-panel textarea#_combo_markdown_editor_0') .fill('@'); - await save_visual(page); + await screenshot(page); await expect(page.locator('ul.suggestions li span:first-of-type')).toContainText([ 'user1', diff --git a/tests/e2e/pr-title.test.e2e.ts b/tests/e2e/pr-title.test.e2e.ts index 390cc81298..2d2c108cb5 100644 --- a/tests/e2e/pr-title.test.e2e.ts +++ b/tests/e2e/pr-title.test.e2e.ts @@ -7,7 +7,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -16,10 +17,10 @@ test('PR: title edit', async ({page}) => { expect(response?.status()).toBe(200); await expect(page.locator('#editable-label')).toBeVisible(); - await save_visual(page); + await screenshot(page); // Labels AGit and Editable are hidden when title is in edit mode await page.locator('#issue-title-edit-show').click(); await expect(page.locator('#editable-label')).toBeHidden(); - await save_visual(page); + await screenshot(page); }); diff --git a/tests/e2e/profile_actions.test.e2e.ts b/tests/e2e/profile_actions.test.e2e.ts index e27ecf64cf..8f4293d749 100644 --- a/tests/e2e/profile_actions.test.e2e.ts +++ b/tests/e2e/profile_actions.test.e2e.ts @@ -6,11 +6,14 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); -test('Follow and block actions', async ({page}) => { +test('Follow and block actions', async ({page}, workerInfo) => { + test.skip(workerInfo.project.name === 'Mobile Safari', 'Mobile Safari is unreliable in this test'); + await page.goto('/user1'); // Check if following and then unfollowing works. @@ -32,7 +35,7 @@ test('Follow and block actions', async ({page}) => { await blockButton.click(); await expect(page.locator('#block-user')).toBeVisible(); - await save_visual(page); + await screenshot(page); await page.locator('#block-user .ok').click(); await expect(blockButton).toContainText('Unblock'); await expect(page.locator('#block-user')).toBeHidden(); @@ -42,7 +45,7 @@ test('Follow and block actions', async ({page}) => { const flashMessage = page.locator('#flash-message'); await expect(flashMessage).toBeVisible(); await expect(flashMessage).toContainText('You cannot follow this user because you have blocked this user or this user has blocked you.'); - await save_visual(page); + await screenshot(page); // Unblock interaction. await actionsDropdownBtn.click(); diff --git a/tests/e2e/reaction-selectors.test.e2e.ts b/tests/e2e/reaction-selectors.test.e2e.ts index 54b7d91869..598369ffa3 100644 --- a/tests/e2e/reaction-selectors.test.e2e.ts +++ b/tests/e2e/reaction-selectors.test.e2e.ts @@ -4,7 +4,8 @@ // @watch end import {expect, type Locator} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -62,5 +63,5 @@ test('Reaction Selectors', async ({page}) => { await toggleReaction(topPicker, 'laugh'); await assertReactionCounts(comment, {'laugh': 2}); - await save_visual(page); + await screenshot(page); }); diff --git a/tests/e2e/relative-time.test.e2e.ts b/tests/e2e/relative-time.test.e2e.ts index ab8bbc19de..19e151fa79 100644 --- a/tests/e2e/relative-time.test.e2e.ts +++ b/tests/e2e/relative-time.test.e2e.ts @@ -24,7 +24,7 @@ test('Relative time after htmx swap', async ({page}, workerInfo) => { const body = page.locator('body'); await body.evaluate( (element) => - new Promise((resolve) => + new Promise((resolve) => element.addEventListener('htmx:afterSwap', () => { resolve(); }), diff --git a/tests/e2e/release.test.e2e.ts b/tests/e2e/release.test.e2e.ts index a4303a7320..3d37b34e77 100644 --- a/tests/e2e/release.test.e2e.ts +++ b/tests/e2e/release.test.e2e.ts @@ -9,12 +9,13 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; import {validate_form} from './shared/forms.ts'; test.use({user: 'user2'}); -test.describe('repo branch protection settings', () => { +test.describe('Releases', () => { test('External Release Attachments', async ({page, isMobile}, workerInfo) => { test.skip(isMobile || workerInfo.project.name === 'webkit'); @@ -33,7 +34,7 @@ test.describe('repo branch protection settings', () => { await page.fill('input[name=attachment-new-name-2]', 'Test'); await page.fill('input[name=attachment-new-exturl-2]', 'https://forgejo.org/'); await page.click('.remove-rel-attach'); - await save_visual(page); + await screenshot(page); await page.click('.button.small.primary'); // Validate release page and click edit @@ -52,7 +53,7 @@ test.describe('repo branch protection settings', () => { await expect(page.locator('.download[open] li:nth-of-type(3)')).toContainText('Test'); await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://forgejo.org/'); - await save_visual(page); + await screenshot(page); await page.locator('.octicon-pencil').first().click(); // Validate edit page and edit the release @@ -67,7 +68,7 @@ test.describe('repo branch protection settings', () => { await expect(page.locator('.attachment_edit:visible')).toHaveCount(4); await page.locator('.attachment_edit:visible').nth(2).fill('Test3'); await page.locator('.attachment_edit:visible').nth(3).fill('https://gitea.com/'); - await save_visual(page); + await screenshot(page); await page.click('.button.small.primary'); // Validate release page and click edit @@ -77,7 +78,7 @@ test.describe('repo branch protection settings', () => { await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://gitea.io/'); await expect(page.locator('.download[open] li:nth-of-type(4)')).toContainText('Test3'); await expect(page.locator('.download[open] li:nth-of-type(4) a')).toHaveAttribute('href', 'https://gitea.com/'); - await save_visual(page); + await screenshot(page); await page.locator('.octicon-pencil').first().click(); }); diff --git a/tests/e2e/rendering-iframe.test.e2e.ts b/tests/e2e/rendering-iframe.test.e2e.ts new file mode 100644 index 0000000000..507d63705c --- /dev/null +++ b/tests/e2e/rendering-iframe.test.e2e.ts @@ -0,0 +1,61 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// web_src/js/markup/external.js +// @watch end + +import {expect} from '@playwright/test'; +import {test} from './utils_e2e.ts'; + +test('iframe renderer shrinks to shorter page', async ({page}, _workerInfo) => { + const previewPath = '/user2/rendering-test/src/branch/master/short.iframehtml'; + + const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'}); + expect(response?.status()).toBe(200); + + const preview = page.locator('iframe.external-render'); + await expect.poll(async () => { + const boundingBox = await preview.boundingBox(); + return boundingBox.height; + }).toBeLessThan(300); +}); + +test('iframe renderer expands to taller page', async ({page}, _workerInfo) => { + const previewPath = '/user2/rendering-test/src/branch/master/tall.iframehtml'; + + const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'}); + expect(response?.status()).toBe(200); + + const preview = page.locator('iframe.external-render'); + await expect.poll(async () => { + const boundingBox = await preview.boundingBox(); + return boundingBox.height; + }).toBeGreaterThan(300); +}); + +test('iframe renderer expands to taller page with absolutely-positioned body', async ({page}, _workerInfo) => { + const previewPath = '/user2/rendering-test/src/branch/master/absolute.iframehtml'; + + const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'}); + expect(response?.status()).toBe(200); + + const preview = page.locator('iframe.external-render'); + await expect.poll(async () => { + const boundingBox = await preview.boundingBox(); + return boundingBox.height; + }).toBeGreaterThan(300); +}); + +test('iframe renderer remains at default height if script breaks', async ({page}, _workerInfo) => { + const previewPath = '/user2/rendering-test/src/branch/master/fail.iframehtml'; + + const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'}); + expect(response?.status()).toBe(200); + + const preview = page.locator('iframe.external-render'); + await expect.poll(async () => { + const boundingBox = await preview.boundingBox(); + return boundingBox.height; + }).toBeCloseTo(300, 0.5); +}); diff --git a/tests/e2e/repo-code.test.e2e.ts b/tests/e2e/repo-code.test.e2e.ts index 9f885958cb..48f881af80 100644 --- a/tests/e2e/repo-code.test.e2e.ts +++ b/tests/e2e/repo-code.test.e2e.ts @@ -11,7 +11,8 @@ // @watch end import {expect, type Page} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; import {accessibilityCheck} from './shared/accessibility.ts'; async function assertSelectedLines(page: Page, nums: string[]) { @@ -36,7 +37,9 @@ async function assertSelectedLines(page: Page, nums: string[]) { return pageAssertions(); } -test('Line Range Selection', async ({page}) => { +test('Line Range Selection', async ({page}, workerInfo) => { + test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'Unreliable in this test'); + const filePath = '/user2/repo1/src/branch/master/README.md?display=source'; const response = await page.goto(filePath); @@ -55,7 +58,7 @@ test('Line Range Selection', async ({page}) => { // out-of-bounds end line await page.goto(`${filePath}#L1-L100`); await assertSelectedLines(page, ['1', '2', '3']); - await save_visual(page); + await screenshot(page); }); test('Readable diff', async ({page}, workerInfo) => { @@ -82,7 +85,7 @@ test('Readable diff', async ({page}, workerInfo) => { await expect(page.getByText(thisDiff.added, {exact: true})).toHaveCSS('background-color', 'rgb(134, 239, 172)'); } } - await save_visual(page); + await screenshot(page); }); test.describe('As authenticated user', () => { @@ -95,14 +98,14 @@ test.describe('As authenticated user', () => { await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/); await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); await accessibilityCheck({page}, ['.commit-header'], [], []); - await save_visual(page); + await screenshot(page); // check second commit await page.goto('/user2/mentions-highlighted/commits/branch/main'); await page.locator('tbody').getByRole('link', {name: 'Another commit which mentions'}).click(); await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/); await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); await accessibilityCheck({page}, ['.commit-header'], [], []); - await save_visual(page); + await screenshot(page); }); }); diff --git a/tests/e2e/repo-commitgraph.test.e2e.ts b/tests/e2e/repo-commitgraph.test.e2e.ts index e8b85c5997..716b77adee 100644 --- a/tests/e2e/repo-commitgraph.test.e2e.ts +++ b/tests/e2e/repo-commitgraph.test.e2e.ts @@ -5,7 +5,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test('Commit graph overflow', async ({page}) => { const response = await page.goto('/user2/repo1/graph'); @@ -28,7 +29,7 @@ test('Commit graph overflow', async ({page}) => { await expect(page.getByRole('button', {name: 'Mono'})).toBeInViewport({ratio: 1}); await expect(page.getByRole('button', {name: 'Color'})).toBeInViewport({ratio: 1}); await expect(page.locator('.selection.search.dropdown')).toBeInViewport({ratio: 1}); - await save_visual(page); + await screenshot(page); }); test('Switch branch', async ({page}) => { @@ -45,5 +46,5 @@ test('Switch branch', async ({page}) => { await expect(page.locator('#loading-indicator')).toBeHidden(); await expect(page.locator('#rel-container')).toBeVisible(); await expect(page.locator('#rev-container')).toBeVisible(); - await save_visual(page); + await screenshot(page); }); diff --git a/tests/e2e/repo-files.test.e2e.ts b/tests/e2e/repo-files.test.e2e.ts new file mode 100644 index 0000000000..4ec359b989 --- /dev/null +++ b/tests/e2e/repo-files.test.e2e.ts @@ -0,0 +1,100 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// @watch start +// templates/repo/editor/** +// web_src/js/features/common-global.js +// routers/web/web.go +// services/repository/files/** +// @watch end + +import {expect} from '@playwright/test'; +import {test, dynamic_id} from './utils_e2e.ts'; + +test.use({user: 'user2'}); + +interface TestCase { + description: string; + files: string[]; +} + +async function doUpload({page}, testCase: TestCase) { + await page.goto(`/user2/file-uploads/_upload/main/`); + const testID = dynamic_id(); + const dropzone = page.getByRole('button', {name: 'Drop files or click here to upload.'}); + + // create the virtual files + const dataTransfer = await page.evaluateHandle((testCase: TestCase) => { + const dt = new DataTransfer(); + for (const filename of testCase.files) { + dt.items.add(new File([`File content of ${filename}`], filename, {type: 'text/plain'})); + } + return dt; + }, testCase); + // and drop them to the upload area + await dropzone.dispatchEvent('drop', {dataTransfer}); + + await page.getByText('new branch').click(); + + await page.getByRole('textbox', {name: 'Name the new branch for this'}).fill(testID); + // ToDo: Potential race condition: We do not currently wait for the upload to complete. + // See https://codeberg.org/forgejo/forgejo/pulls/6687#issuecomment-5068272 and + // https://codeberg.org/forgejo/forgejo/issues/5893#issuecomment-5068266 for details. + // Workaround is to wait (the uploads are just a few bytes and usually complete instantly) + // + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(100); + + await page.getByRole('button', {name: 'Propose file change'}).click(); +} + +test.describe('Drag and Drop upload', () => { + const goodTestCases: TestCase[] = [ + { + description: 'normal and special characters', + files: [ + 'dir1/file1.txt', + 'double/nested/file.txt', + 'special/äüöÄÜÖß.txt', + 'special/Ʉ₦ł₵ØĐɆ.txt', + ], + }, + { + description: 'strange paths and spaces', + files: [ + '..dots.txt', + '.dots.preserved.txt', + 'special/S P A C E !.txt', + ], + }, + ]; + + // actual good tests based on definition above + for (const testCase of goodTestCases) { + test(`good: ${testCase.description}`, async ({page}) => { + await doUpload({page}, testCase); + + // check that nested file structure is preserved + for (const filename of testCase.files) { + await expect(page.locator('#diff-file-boxes').getByRole('link', {name: filename})).toBeVisible(); + } + }); + } + + const badTestCases: TestCase[] = [ + { + description: 'broken path slash in front', + files: [ + '/special/badfirstslash.txt', + ], + }, + ]; + + // actual bad tests based on definition above + for (const testCase of badTestCases) { + test(`bad: ${testCase.description}`, async ({page}) => { + await doUpload({page}, testCase); + await expect(page.getByText('Failed to upload files to')).toBeVisible(); + }); + } +}); diff --git a/tests/e2e/repo-home.test.e2e.ts b/tests/e2e/repo-home.test.e2e.ts index 6f3d6c373b..53fcdd77f1 100644 --- a/tests/e2e/repo-home.test.e2e.ts +++ b/tests/e2e/repo-home.test.e2e.ts @@ -1,25 +1,34 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + // @watch start // web_src/js/components/RepoBranchTagSelector.vue // web_src/js/features/common-global.js // web_src/css/repo.css +// web_src/css/modules/stats-bar.css // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; + +test('Language stats bar', async ({browser}) => { + // This test doesn't need JS and runs a little faster without it + const context = await browser.newContext({javaScriptEnabled: false}); + const page = await context.newPage(); -test('Language stats bar', async ({page}) => { const response = await page.goto('/user2/language-stats-test'); expect(response?.status()).toBe(200); - await expect(page.locator('#language-stats-legend')).toBeHidden(); + await expect(page.locator('#language-stats ul')).toBeHidden(); - await page.click('#language-stats-bar'); - await expect(page.locator('#language-stats-legend')).toBeVisible(); - await save_visual(page); + await page.click('#language-stats summary'); + await expect(page.locator('#language-stats ul')).toBeVisible(); + await screenshot(page); - await page.click('#language-stats-bar'); - await expect(page.locator('#language-stats-legend')).toBeHidden(); - await save_visual(page); + await page.click('#language-stats summary'); + await expect(page.locator('#language-stats ul')).toBeHidden(); + await screenshot(page); }); test('Branch selector commit icon', async ({page}) => { diff --git a/tests/e2e/repo-labels.test.e2e.ts b/tests/e2e/repo-labels.test.e2e.ts new file mode 100644 index 0000000000..e7d5ce32bf --- /dev/null +++ b/tests/e2e/repo-labels.test.e2e.ts @@ -0,0 +1,43 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// templates/repo/issues/labels/** +// web_src/js/features/comp/LabelEdit.js +// @watch end + +import {expect} from '@playwright/test'; +import {test, dynamic_id} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; + +test.use({user: 'user2'}); + +test('New label', async ({page}) => { + const response = await page.goto('/user2/repo1/labels'); + expect(response?.status()).toBe(200); + + await page.getByRole('button', {name: 'New label'}).click(); + await expect(page.locator('#new-label-modal')).toBeVisible(); + await screenshot(page, page.locator('#new-label-modal')); + + const labelName = dynamic_id(); + await page.keyboard.type(labelName); + await page.getByRole('button', {name: 'Create label'}).click(); + + await page.locator('.label-title').filter({hasText: labelName}).isVisible(); +}); + +test('Edit label', async ({page}) => { + const response = await page.goto('/user2/repo1/labels'); + expect(response?.status()).toBe(200); + + await page.getByText('Edit').first().click(); + await expect(page.locator('#edit-label-modal')).toBeVisible(); + await screenshot(page, page.locator('#edit-label-modal')); + + const labelName = dynamic_id(); + await page.keyboard.type(labelName); + await page.getByRole('button', {name: 'Save'}).click(); + + await page.locator('.label-title').filter({hasText: labelName}).isVisible(); +}); diff --git a/tests/e2e/repo-migrate.test.e2e.ts b/tests/e2e/repo-migrate.test.e2e.ts index f0be73e777..26619547ae 100644 --- a/tests/e2e/repo-migrate.test.e2e.ts +++ b/tests/e2e/repo-migrate.test.e2e.ts @@ -3,7 +3,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, test_context, dynamic_id} from './utils_e2e.ts'; +import {test, test_context, dynamic_id} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.use({user: 'user2'}); @@ -22,7 +23,7 @@ test('Migration type seleciton screen', async ({page}) => { await expect(page.locator('svg.gitea-gitbucket')).toBeVisible(); await expect(page.locator('svg.gitea-codebase')).toBeVisible(); - await save_visual(page); + await screenshot(page); }); test('Migration Repo Name detection', async ({page}, workerInfo) => { @@ -50,7 +51,7 @@ test('Migration Repo Name detection', async ({page}, workerInfo) => { await expect(form.getByRole('textbox', {name: 'Repository Name'})).toHaveValue('test'); // Save screenshot only once - await save_visual(page); + await screenshot(page); }); test('Migration Progress Page', async ({page, browser}, workerInfo) => { @@ -64,10 +65,10 @@ test('Migration Progress Page', async ({page, browser}, workerInfo) => { const form = page.locator('form'); await form.getByRole('textbox', {name: 'Repository Name'}).fill(repoName); await form.getByRole('textbox', {name: 'Migrate / Clone from URL'}).fill(`https://codeberg.org/forgejo/${repoName}`); - await save_visual(page); + await screenshot(page); await form.locator('button.primary').click({timeout: 5000}); await expect(page).toHaveURL(`user2/${repoName}`); - await save_visual(page); + await screenshot(page); const ctx = await test_context(browser, {storageState: {cookies: [], origins: []}}); const unauthenticatedPage = await ctx.newPage(); @@ -76,13 +77,13 @@ test('Migration Progress Page', async ({page, browser}, workerInfo) => { await page.reload(); await expect(page.locator('#repo_migrating_failed')).toBeVisible(); - await save_visual(page); + await screenshot(page); await page.getByRole('button', {name: 'Delete this repository'}).click(); const deleteModal = page.locator('#delete-repo-modal'); await deleteModal.getByRole('textbox', {name: 'Confirmation string'}).fill(`user2/${repoName}`); - await save_visual(page); + await screenshot(page); await deleteModal.getByRole('button', {name: 'Delete repository'}).click(); await expect(page).toHaveURL('/'); // checked last to preserve the order of screenshots from first run - await save_visual(unauthenticatedPage); + await screenshot(unauthenticatedPage); }); diff --git a/tests/e2e/repo-new.test.e2e.ts b/tests/e2e/repo-new.test.e2e.ts index ed5e0beb00..bbe0652fba 100644 --- a/tests/e2e/repo-new.test.e2e.ts +++ b/tests/e2e/repo-new.test.e2e.ts @@ -4,7 +4,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, dynamic_id, save_visual} from './utils_e2e.ts'; +import {test, dynamic_id} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; import {validate_form} from './shared/forms.ts'; test.use({user: 'user2'}); @@ -17,12 +18,12 @@ test('New repo: invalid', async ({page}) => { await expect(page.getByText('.gitignore Select .gitignore')).toBeHidden(); await expect(page.getByText('Labels Select a label set')).toBeHidden(); await validate_form({page}, 'fieldset'); - await save_visual(page); + await screenshot(page); await page.getByLabel('Repository name').fill('*invalid'); await page.getByRole('button', {name: 'Create repository'}).click(); await expect(page.getByText('Repository name should contain only alphanumeric')).toBeVisible(); - await save_visual(page); + await screenshot(page); }); test('New repo: initialize', async ({page}, workerInfo) => { @@ -46,7 +47,7 @@ test('New repo: initialize', async ({page}, workerInfo) => { await page.getByLabel('Make repository a template').check(); await validate_form({page}, 'fieldset'); - await save_visual(page); + await screenshot(page); const reponame = dynamic_id(); await page.getByLabel('Repository name').fill(reponame); await page.getByRole('button', {name: 'Create repository'}).click(); @@ -55,7 +56,7 @@ test('New repo: initialize', async ({page}, workerInfo) => { if (!workerInfo.project.name.includes('Mobile')) { await expect(page.getByText('Template', {exact: true})).toBeVisible(); } - await save_visual(page); + await screenshot(page); }); test('New repo: initialize later', async ({page}) => { @@ -72,7 +73,7 @@ test('New repo: initialize later', async ({page}) => { expect(page.url()).toBe(`http://localhost:3003/user2/${reponame}`); await expect(page.getByRole('link', {name: 'New file'})).toBeVisible(); await expect(page.getByRole('heading', {name: 'Creating a new repository on'})).toBeVisible(); - await save_visual(page); + await screenshot(page); // add a README await page.getByRole('link', {name: 'New file'}).click(); @@ -89,7 +90,7 @@ test('New repo: initialize later', async ({page}) => { expect(page.url()).toBe(`http://localhost:3003/user2/${reponame}/src/branch/devbranch/README.md`); await expect(page.getByRole('link', {name: 'My first commit message'})).toBeVisible(); await expect(page.getByText('Hello Forgejo!')).toBeVisible(); - await save_visual(page); + await screenshot(page); }); test('New repo: from template', async ({page}, workerInfo) => { @@ -101,11 +102,11 @@ test('New repo: from template', async ({page}, workerInfo) => { await page.getByRole('group', {name: 'Use a template You can select'}).getByRole('combobox').click(); await page.getByRole('option', {name: 'user27/template1'}).click(); await page.getByText('Git content (Default branch)').click(); - await save_visual(page); + await screenshot(page); await page.getByLabel('Repository name').fill(reponame); await page.getByRole('button', {name: 'Create repository'}).click(); await expect(page.getByRole('link', {name: `${reponame}.log`})).toBeVisible(); - await save_visual(page); + await screenshot(page); }); test('New repo: label set', async ({page}) => { @@ -117,13 +118,13 @@ test('New repo: label set', async ({page}) => { await page.getByRole('option', {name: 'Advanced (Kind/Bug, Kind/'}).click(); // close dropdown via unrelated click await page.getByText('You can select an existing').click(); - await save_visual(page); + await screenshot(page); await page.getByLabel('Repository name').fill(reponame); await page.getByRole('button', {name: 'Create repository'}).click(); await page.goto(`/user2/${reponame}/issues`); await page.getByRole('link', {name: 'Labels'}).click(); await expect(page.getByText('Kind/Bug Something is not')).toBeVisible(); - await save_visual(page); + await screenshot(page); }); test('New repo: gitignore', async ({page}) => { @@ -140,7 +141,7 @@ test('New repo: gitignore', async ({page}) => { await page.getByRole('option', {name: 'NotesAndExtendedConfiguration'}).click(); await page.getByRole('option', {name: 'MetaProgrammingSystem'}).click(); await page.getByRole('option', {name: 'AppceleratorTitanium'}).click(); - await save_visual(page); + await screenshot(page); const segmentWidth = (await page.locator('.segment').boundingBox()).width; const dropdownWidth = (await gitignoreDropdown.boundingBox()).width; diff --git a/tests/e2e/repo-settings.test.e2e.ts b/tests/e2e/repo-settings.test.e2e.ts index 547b015921..aa57c45e8e 100644 --- a/tests/e2e/repo-settings.test.e2e.ts +++ b/tests/e2e/repo-settings.test.e2e.ts @@ -7,7 +7,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; import {validate_form} from './shared/forms.ts'; test.use({user: 'user2'}); @@ -21,13 +22,13 @@ test('repo webhook settings', async ({page}) => { // check accessibility including the custom events (now visible) part await validate_form({page}, 'fieldset'); - await save_visual(page); + await screenshot(page); await page.locator('input[name="events"][value="push_only"]').click(); await expect(page.locator('.hide-unless-checked')).toBeHidden(); await page.locator('input[name="events"][value="send_everything"]').click(); await expect(page.locator('.hide-unless-checked')).toBeHidden(); - await save_visual(page); + await screenshot(page); }); test.describe('repo branch protection settings', () => { @@ -53,11 +54,11 @@ test.describe('repo branch protection settings', () => { // verify header is new await expect(page.locator('h4')).toContainText('new'); await page.locator('input[name="rule_name"]').fill('testrule'); - await save_visual(page); + await screenshot(page); await page.locator('button:text("Save rule")').click(); // verify header is in edit mode await page.waitForLoadState('domcontentloaded'); - await save_visual(page); + await screenshot(page); // find the edit button and click it const editButton = page.locator('a[href="/user2/repo1/settings/branches/edit?rule_name=testrule"]'); @@ -65,6 +66,6 @@ test.describe('repo branch protection settings', () => { await page.waitForLoadState(); await expect(page.locator('.repo-setting-content .header')).toContainText('Protection rules for branch', {ignoreCase: true, useInnerText: true}); - await save_visual(page); + await screenshot(page); }); }); diff --git a/tests/e2e/repo-wiki.test.e2e.ts b/tests/e2e/repo-wiki.test.e2e.ts index 61bb2ad181..02ada9e6cc 100644 --- a/tests/e2e/repo-wiki.test.e2e.ts +++ b/tests/e2e/repo-wiki.test.e2e.ts @@ -4,7 +4,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; for (const searchTerm of ['space', 'consectetur']) { for (const width of [null, 2560, 4000]) { @@ -25,7 +26,7 @@ for (const searchTerm of ['space', 'consectetur']) { await expect(page.locator('#wiki-search a[href]')).toBeInViewport({ ratio: workerInfo.project.name === 'webkit' ? 0.9 : 1, }); - await save_visual(page); + await screenshot(page); }); } } @@ -39,12 +40,12 @@ test(`Search results show titles (and not file names)`, async ({page}, workerInf // so we manually "type" the last letter await page.getByPlaceholder('Search wiki').dispatchEvent('keyup'); await expect(page.locator('#wiki-search a[href] b')).toHaveText('Page With Spaced Name'); - await save_visual(page); + await screenshot(page); }); test('Wiki unicode-escape', async ({page}) => { await page.goto('/user2/unicode-escaping/wiki'); - await save_visual(page); + await screenshot(page); expect(await page.locator('.ui.message.unicode-escape-prompt').count()).toEqual(3); diff --git a/tests/e2e/right-settings-button.test.e2e.ts b/tests/e2e/right-settings-button.test.e2e.ts index 3bea329ba0..8daeddacf2 100644 --- a/tests/e2e/right-settings-button.test.e2e.ts +++ b/tests/e2e/right-settings-button.test.e2e.ts @@ -5,7 +5,8 @@ // @watch end import {expect} from '@playwright/test'; -import {save_visual, test} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test.describe('desktop viewport as user 2', () => { test.use({user: 'user2', viewport: {width: 1920, height: 300}}); @@ -54,7 +55,7 @@ test.describe('desktop viewport, unauthenticated', () => { await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0); await expect(page.locator('.overflow-menu-button')).toHaveCount(0); - await save_visual(page); + await screenshot(page); }); }); @@ -79,7 +80,7 @@ test.describe('small viewport', () => { const items = shownItems.concat(overflowItems); expect(Array.from(new Set(items))).toHaveLength(items.length); - await save_visual(page); + await screenshot(page); }); test('Settings button in overflow menu of org header', async ({page}) => { @@ -123,6 +124,6 @@ test.describe('small viewport, unauthenticated', () => { const items = shownItems.concat(overflowItems); expect(Array.from(new Set(items))).toHaveLength(items.length); - await save_visual(page); + await screenshot(page); }); }); diff --git a/tests/e2e/shared/screenshots.ts b/tests/e2e/shared/screenshots.ts new file mode 100644 index 0000000000..cf9788e366 --- /dev/null +++ b/tests/e2e/shared/screenshots.ts @@ -0,0 +1,89 @@ +import {expect, type Page, type Locator} from '@playwright/test'; + +// returns element that should be covered before taking the screenshot +async function masks(page: Page) : Promise { + return [ + page.locator('.ui.avatar'), + page.locator('.sha'), + page.locator('#repo_migrating'), + // update order of recently created repos is not fully deterministic + page.locator('.flex-item-main').filter({hasText: 'relative time in repo'}), + page.locator('#activity-feed'), + page.locator('#user-heatmap'), + // dynamic IDs in fixed-size inputs + page.locator('input[value*="dyn-id-"]'), + ]; +} + +// replaces elements on the page that cause flakiness +async function screenshot_prepare(page: Page) { + await page.waitForLoadState('domcontentloaded'); + // Version string is dynamic + await page.locator('footer .left-links').evaluate((node) => node.innerHTML = 'MOCK'); + + // replace timestamps in repos to mask them later down + await page.locator('.flex-item-body > relative-time').filter({hasText: /now|minute/}).evaluateAll((nodes) => { + for (const node of nodes) node.outerHTML = 'relative time in repo'; + }); + // other time elements + await page.locator('relative-time').evaluateAll((nodes) => { + for (const node of nodes) node.outerHTML = 'time element'; + }); + await page.locator('absolute-date').evaluateAll((nodes) => { + for (const node of nodes) node.outerHTML = 'time element'; + }); + + // dynamically generated UUIDs + await page.getByText('dyn-id-').evaluateAll((nodes) => { + for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id'); + }); + // repeat above, work around https://github.com/microsoft/playwright/issues/34152 + await page.getByText('dyn-id-').evaluateAll((nodes) => { + for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id'); + }); + + // attachment IDs in text areas, required for issue-comment-dropzone. + // playwright does not (yet?) support filtering for content in input elements, see https://github.com/microsoft/playwright/issues/36166 + await page.locator('textarea.markdown-text-editor').evaluateAll((nodes: HTMLTextAreaElement[]) => { + for (const node of nodes) node.value = node.value.replaceAll(/attachments\/[a-f0-9-]+/g, '/attachments/c1ee9740-dad3-4747-b489-f6fb2e3dfcec'); + }); + + // dynamically created test users + await page.getByText('e2e-test-').evaluateAll((nodes) => { + for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/e2e-test-[0-9-]+/g, 'e2e-test-user'); + }); +} + +export async function screenshot(page: Page, locator?: Locator, margin = 0) { + // Optionally include visual testing + if (process.env.VISUAL_TEST) { + await screenshot_prepare(page); + if (locator === undefined) { + await screenshot_full(page); + } else { + await screenshot_selective(page, locator, margin); + } + } +} + +async function screenshot_selective(page: Page, locator: Locator, margin: number) { + const clip = await locator.boundingBox(); + clip.x = Math.max(clip.x - margin, 0); + clip.y = Math.max(clip.y - margin, 0); + clip.width += margin * 2; + clip.height += margin * 2; + await expect(page).toHaveScreenshot({ + fullPage: true, + timeout: 20000, + clip, + mask: await masks(page), + }); +} + +async function screenshot_full(page: Page) { + await expect(page).toHaveScreenshot({ + fullPage: true, + timeout: 20000, + mask: await masks(page), + }); +} diff --git a/tests/e2e/switch.test.e2e.ts b/tests/e2e/switch.test.e2e.ts index 217a6576b9..6c7b33bbf6 100644 --- a/tests/e2e/switch.test.e2e.ts +++ b/tests/e2e/switch.test.e2e.ts @@ -3,6 +3,7 @@ // @watch start // web_src/css/modules/switch.css +// web_src/css/modules/button.css // web_src/css/themes // @watch end @@ -68,7 +69,7 @@ test('Switch CSS properties', async ({browser}) => { // E2E already runs clients with both fine and coarse pointer simulated // This test will verify that coarse-related CSS is working as intended - const itemHeight = await page.evaluate(() => window.matchMedia('(pointer: coarse)').matches) ? 41 : 34; + const itemHeight = await page.evaluate(() => window.matchMedia('(pointer: coarse)').matches) ? 38 : 34; // In Firefox Math.round is needed because .height is 33.99998474121094 expect(Math.round((await page.locator('#issue-filters .switch > .item:nth-child(1)').boundingBox()).height)).toBe(itemHeight); }); diff --git a/tests/e2e/user-settings.test.e2e.ts b/tests/e2e/user-settings.test.e2e.ts index 88e6708115..22cec4c8cc 100644 --- a/tests/e2e/user-settings.test.e2e.ts +++ b/tests/e2e/user-settings.test.e2e.ts @@ -4,7 +4,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, login_user, login} from './utils_e2e.ts'; +import {test, login_user, login} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; import {validate_form} from './shared/forms.ts'; test.beforeAll(async ({browser}, workerInfo) => { @@ -22,7 +23,7 @@ test('User: Profile settings', async ({browser}, workerInfo) => { await pronounsInput.click(); const pronounsList = page.locator('datalist#pronouns'); const pronounsOptions = pronounsList.locator('option'); - const pronounsValues = await pronounsOptions.evaluateAll((opts) => opts.map((opt) => opt.value)); + const pronounsValues = await pronounsOptions.evaluateAll((opts) => opts.map((opt: HTMLOptionElement) => opt.value)); expect(pronounsValues).toEqual(['he/him', 'she/her', 'they/them', 'it/its', 'any pronouns']); await pronounsInput.fill('she/her'); @@ -32,16 +33,16 @@ test('User: Profile settings', async ({browser}, workerInfo) => { await page.getByPlaceholder('Share your approximate').fill('on a computer chip'); await page.getByLabel('User visibility').click(); await page.getByLabel('Visible only to signed-in').click(); - await page.getByLabel('Hide email address Your email').uncheck(); + await page.getByLabel('Hide email address Email address will').uncheck(); await page.getByLabel('Hide activity from profile').check(); await validate_form({page}, 'fieldset'); - await save_visual(page); + await screenshot(page); await page.getByRole('button', {name: 'Update profile'}).click(); await expect(page.getByText('Your profile has been updated.')).toBeVisible(); await page.getByRole('link', {name: 'public activity'}).click(); await expect(page.getByText('Your activity is only visible')).toBeVisible(); - await save_visual(page); + await screenshot(page); await page.goto('/user2'); await expect(page.getByText('SecondUser')).toBeVisible(); @@ -49,17 +50,17 @@ test('User: Profile settings', async ({browser}, workerInfo) => { await expect(page.locator('li').filter({hasText: 'user2@example.com'})).toBeVisible(); await expect(page.locator('li').filter({hasText: 'https://forgejo.org'})).toBeVisible(); await expect(page.getByText('I am a playwright test')).toBeVisible(); - await save_visual(page); + await screenshot(page); await page.goto('/user/settings'); await page.locator('input[list="pronouns"]').fill('rob/ot'); await page.getByLabel('User visibility').click(); await page.getByLabel('Visible to everyone').click(); - await page.getByLabel('Hide email address Your email').check(); + await page.getByLabel('Hide email address Email address will').check(); await page.getByLabel('Hide activity from profile').uncheck(); await expect(page.getByText('Your profile has been updated.')).toBeHidden(); await validate_form({page}, 'fieldset'); - await save_visual(page); + await screenshot(page); await page.getByRole('button', {name: 'Update profile'}).click(); await expect(page.getByText('Your profile has been updated.')).toBeVisible(); @@ -75,8 +76,22 @@ test('User: Storage overview', async ({browser}, workerInfo) => { await page.goto('/user/settings/storage_overview'); await page.waitForLoadState(); await page.getByLabel('Git LFS – 8 KiB').nth(1).hover({position: {x: 250, y: 2}}); - await expect(page.getByText('Git LFS')).toBeVisible(); - await save_visual(page); + await expect(page.getByText('Git LFS – 8 KiB')).toBeVisible(); + + // Show/hide legend by clicking on the bar + await expect(page.locator('.stats ul').nth(1)).toBeHidden(); + await expect(page.getByText('Git LFS 8 KiB').nth(1)).toBeHidden(); + + await page.locator('.stats summary').nth(1).click(); + await expect(page.locator('.stats ul').nth(1)).toBeVisible(); + await expect(page.getByText('Git LFS 8 KiB').nth(1)).toBeVisible(); + await screenshot(page); + + await page.locator('.stats summary').nth(1).click(); + await expect(page.locator('.stats ul').nth(1)).toBeHidden(); + await expect(page.getByText('Git LFS 8 KiB').nth(1)).toBeHidden(); + + await screenshot(page); }); test('User: Canceling adding SSH key clears inputs', async ({browser}, workerInfo) => { @@ -109,3 +124,24 @@ test('User: Canceling adding GPG key clears input', async ({browser}, workerInfo await expect(gpgKeyContent).toHaveValue(''); }); + +test('User: Add access token', async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); + await page.goto('/user/settings/applications'); + + await page.locator('#scoped-access-submit').click(); + await page.locator('#name:invalid').isVisible(); + + await page.locator('details.optional.field').click(); + await page.selectOption('#access-token-scope-activitypub', 'read:activitypub'); + await page.locator('#scoped-access-submit').click(); + + await page.locator('#name:invalid').isVisible(); + await expect(page.locator('#access-token-scope-activitypub')).toHaveValue('read:activitypub'); + + const tokenName = globalThis.crypto.randomUUID(); + await page.locator('#name').fill(tokenName); + await page.locator('#scoped-access-submit').click(); + + await page.getByText(tokenName).isVisible(); +}); diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts index ff921a2cf3..a0eea19329 100644 --- a/tests/e2e/utils_e2e.ts +++ b/tests/e2e/utils_e2e.ts @@ -1,4 +1,4 @@ -import {expect, test as baseTest, type Browser, type BrowserContextOptions, type APIRequestContext, type TestInfo, type Page} from '@playwright/test'; +import {expect, test as baseTest, type Browser, type BrowserContextOptions, type APIRequestContext, type TestInfo} from '@playwright/test'; import * as path from 'node:path'; @@ -43,10 +43,10 @@ const LOGIN_PASSWORD = 'password'; // log in user and store session info. This should generally be // run in test.beforeAll(), then the session can be loaded in tests. -export async function login_user(browser: Browser, workerInfo: TestInfo, user: string) { +export async function login_user(browser: Browser, workerInfo: TestInfo, user: string, options?: BrowserContextOptions) { test.setTimeout(60000); // Set up a new context - const context = await test_context(browser); + const context = await test_context(browser, options); const page = await context.newPage(); // Route to login page @@ -84,49 +84,6 @@ export async function login({browser}: {browser: Browser}, workerInfo: TestInfo) return await context?.newPage(); } -export async function save_visual(page: Page) { - // Optionally include visual testing - if (process.env.VISUAL_TEST) { - await page.waitForLoadState('domcontentloaded'); - // Mock/replace dynamic content which can have different size (and thus cannot simply be masked below) - await page.locator('footer .left-links').evaluate((node) => node.innerHTML = 'MOCK'); - // replace timestamps in repos to mask them later down - await page.locator('.flex-item-body > relative-time').filter({hasText: /now|minute/}).evaluateAll((nodes) => { - for (const node of nodes) node.outerHTML = 'relative time in repo'; - }); - // dynamically generated UUIDs - await page.getByText('dyn-id-').evaluateAll((nodes) => { - for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id'); - }); - // repeat above, work around https://github.com/microsoft/playwright/issues/34152 - await page.getByText('dyn-id-').evaluateAll((nodes) => { - for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id'); - }); - await page.locator('relative-time').evaluateAll((nodes) => { - for (const node of nodes) node.outerHTML = 'time element'; - }); - // used for instance for security keys - await page.locator('absolute-date').evaluateAll((nodes) => { - for (const node of nodes) node.outerHTML = 'time element'; - }); - await expect(page).toHaveScreenshot({ - fullPage: true, - timeout: 20000, - mask: [ - page.locator('.ui.avatar'), - page.locator('.sha'), - page.locator('#repo_migrating'), - // update order of recently created repos is not fully deterministic - page.locator('.flex-item-main').filter({hasText: 'relative time in repo'}), - page.locator('#activity-feed'), - page.locator('#user-heatmap'), - // dynamic IDs in fixed-size inputs - page.locator('input[value*="dyn-id-"]'), - ], - }); - } -} - // Create a temporary user and login to that user and store session info. // This should ideally run on a per test basis. export async function create_temp_user(browser: Browser, workerInfo: TestInfo, request: APIRequestContext) { diff --git a/tests/e2e/webauthn.test.e2e.ts b/tests/e2e/webauthn.test.e2e.ts index d4b81621d2..85337384f4 100644 --- a/tests/e2e/webauthn.test.e2e.ts +++ b/tests/e2e/webauthn.test.e2e.ts @@ -8,7 +8,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, create_temp_user, login_user} from './utils_e2e.ts'; +import {test, create_temp_user, login_user} from './utils_e2e.ts'; +import {screenshot} from './shared/screenshots.ts'; test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => { test.skip(workerInfo.project.name !== 'chromium', 'Uses Chrome protocol'); @@ -34,13 +35,15 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => }); await page.locator('input#nickname').fill('Testing Security Key'); - await save_visual(page); + await screenshot(page, page.locator('.user-setting-content')); await page.getByText('Add security key').click(); + await expect(page.getByRole('button', {name: 'Remove'})).toBeVisible(); // "Remove" button is visible, indicating that the security key was added // Logout. + await page.locator('div[aria-label="Profile and settings…"]').click(); + await page.getByText('Sign out').click(); await expect(async () => { - await page.locator('div[aria-label="Profile and settings…"]').click(); - await page.getByText('Sign out').click(); + await page.waitForURL(`${workerInfo.project.use.baseURL}/`); }).toPass(); // Login. @@ -57,8 +60,9 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => response = await page.goto('/user/settings/security'); expect(response?.status()).toBe(200); await page.getByRole('button', {name: 'Remove'}).click(); - await save_visual(page); + await screenshot(page, page.locator('.ui.g-modal-confirm.delete.modal'), 50); await page.getByRole('button', {name: 'Yes'}).click(); + await expect(page.getByRole('button', {name: 'Remove'})).toBeHidden(); await page.waitForLoadState(); // verify the user can login without a key diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/HEAD b/tests/gitea-repositories-meta/user2/rendering-test.git/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/tests/gitea-repositories-meta/user2/rendering-test.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/config b/tests/gitea-repositories-meta/user2/rendering-test.git/config new file mode 100644 index 0000000000..07d359d07c --- /dev/null +++ b/tests/gitea-repositories-meta/user2/rendering-test.git/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/description b/tests/gitea-repositories-meta/user2/rendering-test.git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/tests/gitea-repositories-meta/user2/rendering-test.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/info/exclude b/tests/gitea-repositories-meta/user2/rendering-test.git/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/rendering-test.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/29/262fe430e92c957b5d6945df90570d85725dfd b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/29/262fe430e92c957b5d6945df90570d85725dfd new file mode 100644 index 0000000000..184f27bb65 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/29/262fe430e92c957b5d6945df90570d85725dfd differ diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/32/6ad6b850d0786dcbe128fb1cc2754ea739d501 b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/32/6ad6b850d0786dcbe128fb1cc2754ea739d501 new file mode 100644 index 0000000000..6d9a33fdcf Binary files /dev/null and b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/32/6ad6b850d0786dcbe128fb1cc2754ea739d501 differ diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/35/5dfe24938c3f06a4c5078fa3bd5f3c65c17c8f b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/35/5dfe24938c3f06a4c5078fa3bd5f3c65c17c8f new file mode 100644 index 0000000000..d275362b93 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/35/5dfe24938c3f06a4c5078fa3bd5f3c65c17c8f differ diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/4d/26f069992909b3ff6e52c027f781d0a9eb655c b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/4d/26f069992909b3ff6e52c027f781d0a9eb655c new file mode 100644 index 0000000000..9a2230ae80 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/4d/26f069992909b3ff6e52c027f781d0a9eb655c differ diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/aa/ee434b5dbfce8a1fa9a0e21e7fa4743d7d5c9a b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/aa/ee434b5dbfce8a1fa9a0e21e7fa4743d7d5c9a new file mode 100644 index 0000000000..87ff244ebb --- /dev/null +++ b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/aa/ee434b5dbfce8a1fa9a0e21e7fa4743d7d5c9a @@ -0,0 +1 @@ +x+)JMU047g040031QHL*)-IL+JM(au-Rmv4Wqؼ<-13Yi?=lK/oz8#YQֵ*rO?-soUFڒc_J~!\x{iϖ:sK \ No newline at end of file diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/b7/d263e8137441dc76f7594e358cb47e19ce7fe1 b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/b7/d263e8137441dc76f7594e358cb47e19ce7fe1 new file mode 100644 index 0000000000..02776c78f1 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/b7/d263e8137441dc76f7594e358cb47e19ce7fe1 differ diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/da/dc7a0667d2671fd1b6c6b8814eff2440b3bb3b b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/da/dc7a0667d2671fd1b6c6b8814eff2440b3bb3b new file mode 100644 index 0000000000..0fdf4f9019 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/da/dc7a0667d2671fd1b6c6b8814eff2440b3bb3b differ diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/objects/fa/faad77cb54665ac800d1bf77e6a55bd355eabc b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/fa/faad77cb54665ac800d1bf77e6a55bd355eabc new file mode 100644 index 0000000000..e91b9e0912 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/rendering-test.git/objects/fa/faad77cb54665ac800d1bf77e6a55bd355eabc differ diff --git a/tests/gitea-repositories-meta/user2/rendering-test.git/refs/heads/master b/tests/gitea-repositories-meta/user2/rendering-test.git/refs/heads/master new file mode 100644 index 0000000000..69d0f8e29c --- /dev/null +++ b/tests/gitea-repositories-meta/user2/rendering-test.git/refs/heads/master @@ -0,0 +1 @@ +fafaad77cb54665ac800d1bf77e6a55bd355eabc diff --git a/tests/integration/actions_commit_status_test.go b/tests/integration/actions_commit_status_test.go index 5667bdadd5..05de90e5ab 100644 --- a/tests/integration/actions_commit_status_test.go +++ b/tests/integration/actions_commit_status_test.go @@ -25,7 +25,7 @@ import ( ) func TestActionsAutomerge(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Actions.Enabled, true)() ctx := db.DefaultContext diff --git a/tests/integration/actions_concurrency_group_queue_test.go b/tests/integration/actions_concurrency_group_queue_test.go new file mode 100644 index 0000000000..72520d43b1 --- /dev/null +++ b/tests/integration/actions_concurrency_group_queue_test.go @@ -0,0 +1,281 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package integration + +import ( + "net/url" + "strings" + "testing" + + actions_model "forgejo.org/models/actions" + "forgejo.org/models/db" + unit_model "forgejo.org/models/unit" + "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + "forgejo.org/modules/gitrepo" + "forgejo.org/modules/setting" + "forgejo.org/modules/test" + actions_service "forgejo.org/services/actions" + files_service "forgejo.org/services/repository/files" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestActionConcurrencyRunnerFiltering(t *testing.T) { + defer unittest.OverrideFixtures("tests/integration/fixtures/TestActionConcurrencyRunnerFiltering")() + require.NoError(t, unittest.PrepareTestDatabase()) + + for _, tc := range []struct { + name string + runnerName string + expectedRunIDs []int64 + }{ + { + // owner id 2 + runnerName: "User runner", + expectedRunIDs: []int64{500, 502}, + }, + { + // owner id 3 + runnerName: "Organisation runner", + expectedRunIDs: []int64{501}, + }, + { + runnerName: "Repository runner", + expectedRunIDs: []int64{502}, + }, + { + runnerName: "Global runner", + expectedRunIDs: []int64{500, 501, 502}, + }, + } { + t.Run(tc.runnerName, func(t *testing.T) { + // defer unittest.OverrideFixtures("tests/integration/fixtures/TestActionConcurrencyRunnerFiltering")() + // require.NoError(t, unittest.PrepareTestDatabase()) + + doTest := func() { + e := db.GetEngine(t.Context()) + + runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: tc.runnerName}) + jobs, err := actions_model.GetAvailableJobsForRunner(e, runner) + require.NoError(t, err) + + ids := []int64{} + for _, job := range jobs { + ids = append(ids, job.ID) + } + assert.ElementsMatch(t, tc.expectedRunIDs, ids) + } + + t.Run("ConcurrencyGroupQueueEnabled", func(t *testing.T) { + defer test.MockVariableValue(&setting.Actions.ConcurrencyGroupQueueEnabled, true)() + doTest() + }) + + t.Run("ConcurrencyGroupQueueDisabled", func(t *testing.T) { + defer test.MockVariableValue(&setting.Actions.ConcurrencyGroupQueueEnabled, false)() + doTest() + }) + }) + } +} + +// These tests are a little more unit-testy than they are integration tests, but they're placed in the integration test +// suite so that they're run on all database engines. +func TestActionConcurrencyGroupQueue(t *testing.T) { + for _, tc := range []struct { + name string + expectedRunIDs []int64 + updateRun500 map[string]any + updateRunJob500 map[string]any + updateRun501 map[string]any + updateRunJob501 map[string]any + queuingDisabled bool + }{ + { + name: "queuing disabled", + expectedRunIDs: []int64{500, 501, 502}, + queuingDisabled: true, + }, + { + // Job 501 & 502's data is configured to be queued-behind job 500, so with queuing enabled it shouldn't + // appear. + name: "concurrency blocked", + expectedRunIDs: []int64{500}, + }, + { + name: "different repo", + updateRun501: map[string]any{"repo_id": 2}, + expectedRunIDs: []int64{500, 501}, + }, + { + name: "different concurrency group", + updateRun501: map[string]any{"concurrency_group": "321bca"}, + expectedRunIDs: []int64{500, 501}, + }, + { + name: "null concurrency group", + updateRun501: map[string]any{"concurrency_group": nil}, + expectedRunIDs: []int64{500, 501}, + }, + { + name: "empty concurrency group", + updateRun501: map[string]any{"concurrency_group": ""}, + expectedRunIDs: []int64{500, 501}, + }, + { + name: "unlimited concurrency", + updateRun501: map[string]any{"concurrency_type": actions_model.UnlimitedConcurrency}, + expectedRunIDs: []int64{500, 501}, + }, + { + name: "cancel-in-progress type", + updateRun501: map[string]any{"concurrency_type": actions_model.CancelInProgress}, + expectedRunIDs: []int64{500, 501}, + }, + { + name: "blocking job done", + updateRun500: map[string]any{"status": actions_model.StatusCancelled}, + updateRunJob500: map[string]any{"status": actions_model.StatusCancelled}, + expectedRunIDs: []int64{501}, + }, + { + name: "mid-index job running", + updateRun501: map[string]any{"status": actions_model.StatusRunning}, + updateRunJob501: map[string]any{"status": actions_model.StatusRunning}, + expectedRunIDs: []int64{}, + }, + { + // Reflects a case where 500 may be retried -- there's already a later job (index-wise) in the concurrency + // group that is done, but if 500 is waiting it can still be run + name: "mid-index job ran", + updateRun501: map[string]any{"status": actions_model.StatusSuccess}, + updateRunJob501: map[string]any{"status": actions_model.StatusSuccess}, + expectedRunIDs: []int64{500}, + }, + { + // If both job 500 & job 501 are in the same workflow run, and one is running, the other can still start + // (this would be conditional on its `needs` as a job, but that isn't evaluated by GetAvailableJobsForRunner + // so isn't in the scope of testing here) + name: "multiple jobs from same run", + updateRun500: map[string]any{"status": actions_model.StatusRunning}, + updateRunJob500: map[string]any{"status": actions_model.StatusRunning}, + updateRunJob501: map[string]any{"run_id": 500}, + expectedRunIDs: []int64{501}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + defer unittest.OverrideFixtures("tests/integration/fixtures/TestActionConcurrencyGroupQueue")() + require.NoError(t, unittest.PrepareTestDatabase()) + runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1004}, "owner_id = 0 AND repo_id = 0") + + defer test.MockVariableValue(&setting.Actions.ConcurrencyGroupQueueEnabled, !tc.queuingDisabled)() + + e := db.GetEngine(t.Context()) + + if tc.updateRun500 != nil { + affected, err := e.Table(&actions_model.ActionRun{}).Where("id = ?", 500).Update(tc.updateRun500) + require.NoError(t, err) + require.EqualValues(t, 1, affected) + } + if tc.updateRunJob500 != nil { + affected, err := e.Table(&actions_model.ActionRunJob{}).Where("id = ?", 500).Update(tc.updateRunJob500) + require.NoError(t, err) + require.EqualValues(t, 1, affected) + } + if tc.updateRun501 != nil { + affected, err := e.Table(&actions_model.ActionRun{}).Where("id = ?", 501).Update(tc.updateRun501) + require.NoError(t, err) + require.EqualValues(t, 1, affected) + } + if tc.updateRunJob501 != nil { + affected, err := e.Table(&actions_model.ActionRunJob{}).Where("id = ?", 501).Update(tc.updateRunJob501) + require.NoError(t, err) + require.EqualValues(t, 1, affected) + } + + jobs, err := actions_model.GetAvailableJobsForRunner(e, runner) + require.NoError(t, err) + + ids := []int64{} + for _, job := range jobs { + ids = append(ids, job.ID) + } + assert.ElementsMatch(t, tc.expectedRunIDs, ids) + }) + } +} + +func TestActionConcurrencyGroupQueueFetchNext(t *testing.T) { + if !setting.Database.Type.IsSQLite3() { + // mock repo runner only supported on SQLite testing + t.Skip() + } + + onApplicationRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // create the repo + repo, sha, f := tests.CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch", + []unit_model.Type{unit_model.TypeActions}, nil, + []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".forgejo/workflows/dispatch.yml", + ContentReader: strings.NewReader( + "name: concurrency group workflow\n" + + "on:\n" + + " workflow_dispatch:\n" + + " inputs:\n" + + " ident:\n" + + " type: string\n" + + "concurrency:\n" + + " group: abc\n" + + " cancel-in-progress: false\n" + + "jobs:\n" + + " test:\n" + + " runs-on: ubuntu-latest\n" + + " steps:\n" + + " - run: echo deployment goes here\n"), + }, + }, + ) + defer f() + + gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) + require.NoError(t, err) + defer gitRepo.Close() + + workflow, err := actions_service.GetWorkflowFromCommit(gitRepo, "main", "dispatch.yml") + require.NoError(t, err) + assert.Equal(t, "refs/heads/main", workflow.Ref) + assert.Equal(t, sha, workflow.Commit.ID.String()) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}) + + // first run within the concurrency group + _, _, err = workflow.Dispatch(db.DefaultContext, func(key string) string { return "task1" }, repo, user2) + require.NoError(t, err) + task1 := runner.fetchTask(t) + + // dispatch a second run within the same concurrency group + _, _, err = workflow.Dispatch(db.DefaultContext, func(key string) string { return "task2" }, repo, user2) + require.NoError(t, err) + + // assert that we can't fetch and start that second task -- it's blocked behind the first + task2 := runner.maybeFetchTask(t) + assert.Nil(t, task2) + + // finish the first task + runner.succeedAtTask(t, task1) + + // now task2 should be accessible since task1 has completed + task2 = runner.fetchTask(t) + assert.NotNil(t, task2) + runner.succeedAtTask(t, task2) + }) +} diff --git a/tests/integration/actions_job_test.go b/tests/integration/actions_job_test.go index cd3c95b4a1..7e7cfd0a79 100644 --- a/tests/integration/actions_job_test.go +++ b/tests/integration/actions_job_test.go @@ -129,7 +129,7 @@ jobs: }, }, } - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) @@ -174,6 +174,36 @@ jobs: }) } +func TestRunnerLifecycleGithubEndpoints(t *testing.T) { + if !setting.Database.Type.IsSQLite3() { + // registering a mock runner when using a database other than SQLite leaves leftovers + t.Skip() + } + onApplicationRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiRepo := createActionsTestRepo(t, token, "actions-runner-registration-with-get", false) + runner := newMockRunner() + runner.registerAsRepoRunnerWithPost(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) + runnersList := runner.listRunners(t, user2.Name, apiRepo.Name) + + assert.NotNil(t, runnersList) + assert.Len(t, runnersList.Entries, 1) + assert.Equal(t, "mock-runner", runnersList.Entries[0].Name) + + runnerDetails := runner.getRunner(t, user2.Name, apiRepo.Name, runnersList.Entries[0].ID) + assert.Equal(t, "mock-runner", runnerDetails.Name) + assert.Equal(t, runnersList.Entries[0].ID, runnerDetails.ID) + + runner.deleteRunner(t, user2.Name, apiRepo.Name, runnersList.Entries[0].ID) + + httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository) + doAPIDeleteRepository(httpContext)(t) + }) +} + func TestActionsJobNeedsMatrix(t *testing.T) { if !setting.Database.Type.IsSQLite3() { t.Skip() @@ -318,7 +348,7 @@ jobs: }, }, } - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) @@ -364,7 +394,7 @@ func TestActionsGiteaContext(t *testing.T) { t.Skip() } - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user2Session := loginUser(t, user2.Name) user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) diff --git a/tests/integration/actions_log_test.go b/tests/integration/actions_log_test.go index 0443dc9cfb..4da1114958 100644 --- a/tests/integration/actions_log_test.go +++ b/tests/integration/actions_log_test.go @@ -101,7 +101,7 @@ jobs: zstdEnabled: false, }, } - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) diff --git a/tests/integration/actions_notifications_test.go b/tests/integration/actions_notifications_test.go index ed59c92500..78ab79e72f 100644 --- a/tests/integration/actions_notifications_test.go +++ b/tests/integration/actions_notifications_test.go @@ -59,7 +59,7 @@ jobs: notifyEmail: false, }, } - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) diff --git a/tests/integration/actions_route_test.go b/tests/integration/actions_route_test.go index 930a917524..1218a1abed 100644 --- a/tests/integration/actions_route_test.go +++ b/tests/integration/actions_route_test.go @@ -32,7 +32,7 @@ func GetWorkflowRunRedirectURI(t *testing.T, repoURL, workflow string) string { } func TestActionsWebRouteLatestWorkflowRun(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // create the repo @@ -120,7 +120,7 @@ func TestActionsWebRouteLatestWorkflowRun(t *testing.T) { } func TestActionsWebRouteLatestRun(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // create the repo diff --git a/tests/integration/actions_run_now_done_notification_test.go b/tests/integration/actions_run_now_done_notification_test.go index 480d67a73d..9a2764594b 100644 --- a/tests/integration/actions_run_now_done_notification_test.go +++ b/tests/integration/actions_run_now_done_notification_test.go @@ -72,9 +72,10 @@ func TestActionNowDoneNotification(t *testing.T) { t.Skip() } - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { notifier := mockNotifier{t: t, testIdx: 0, lastRunID: -1, runID: -1} notify_service.RegisterNotifier(¬ifier) + defer notify_service.UnregisterNotifier(¬ifier) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) diff --git a/tests/integration/actions_runner_test.go b/tests/integration/actions_runner_test.go index 859406997e..a9bb122680 100644 --- a/tests/integration/actions_runner_test.go +++ b/tests/integration/actions_runner_test.go @@ -12,6 +12,7 @@ import ( auth_model "forgejo.org/models/auth" "forgejo.org/modules/setting" + "forgejo.org/modules/structs" pingv1 "code.gitea.io/actions-proto-go/ping/v1" "code.gitea.io/actions-proto-go/ping/v1/pingv1connect" @@ -24,7 +25,8 @@ import ( ) type mockRunner struct { - client *mockRunnerClient + client *mockRunnerClient + lastTasksVersion int64 } type mockRunnerClient struct { @@ -82,8 +84,7 @@ func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []strin func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string) { if !setting.Database.Type.IsSQLite3() { - // registering a mock runner when using a database other than SQLite leaves leftovers - t.FailNow() + assert.FailNow(t, "registering a mock runner when using a database other than SQLite leaves leftovers") } session := loginUser(t, ownerName) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) @@ -96,6 +97,70 @@ func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, run r.doRegister(t, runnerName, registrationToken.Token, labels) } +func (r *mockRunner) registerAsRepoRunnerWithPost(t *testing.T, ownerName, repoName, runnerName string, labels []string) { + if !setting.Database.Type.IsSQLite3() { + // registering a mock runner when using a database other than SQLite leaves leftovers + t.FailNow() + } + session := loginUser(t, ownerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var registrationToken struct { + Token string `json:"token"` + } + DecodeJSON(t, resp, ®istrationToken) + r.doRegister(t, runnerName, registrationToken.Token, labels) +} + +func (r *mockRunner) listRunners(t *testing.T, ownerName, repoName string) structs.ActionRunnersResponse { + if !setting.Database.Type.IsSQLite3() { + // registering a mock runner when using a database other than SQLite leaves leftovers + t.FailNow() + } + session := loginUser(t, ownerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners", ownerName, repoName)).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var runnersList structs.ActionRunnersResponse + DecodeJSON(t, resp, &runnersList) + return runnersList +} + +func (r *mockRunner) getRunner(t *testing.T, ownerName, repoName string, runnerID int64) structs.ActionRunner { + if !setting.Database.Type.IsSQLite3() { + // registering a mock runner when using a database other than SQLite leaves leftovers + t.FailNow() + } + session := loginUser(t, ownerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", ownerName, repoName, runnerID)).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var runner structs.ActionRunner + DecodeJSON(t, resp, &runner) + return runner +} + +func (r *mockRunner) deleteRunner(t *testing.T, ownerName, repoName string, runnerID int64) { + if !setting.Database.Type.IsSQLite3() { + // registering a mock runner when using a database other than SQLite leaves leftovers + t.FailNow() + } + session := loginUser(t, ownerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", ownerName, repoName, runnerID)).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) +} + +func (r *mockRunner) maybeFetchTask(t *testing.T) *runnerv1.Task { + resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{ + TasksVersion: r.lastTasksVersion, + })) + require.NoError(t, err) + r.lastTasksVersion = resp.Msg.TasksVersion + return resp.Msg.Task +} + func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task { fetchTimeout := 10 * time.Second if len(timeout) > 0 { @@ -103,13 +168,10 @@ func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1 } var task *runnerv1.Task - assert.Eventually(t, func() bool { - resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{ - TasksVersion: 0, - })) - require.NoError(t, err) - if resp.Msg.Task != nil { - task = resp.Msg.Task + require.Eventually(t, func() bool { + maybeTask := r.maybeFetchTask(t) + if maybeTask != nil { + task = maybeTask return true } return false diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index d13c666020..094ec9cfdd 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -40,7 +40,7 @@ import ( ) func TestActionsPullRequestCommitStatus(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo session := loginUser(t, "user2") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) @@ -351,7 +351,7 @@ jobs: } func TestActionsPullRequestWithInvalidWorkflow(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo session := loginUser(t, "user2") @@ -432,7 +432,7 @@ runs-on: docker } func TestActionsPullRequestTargetEvent(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the base repo org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the forked repo @@ -589,7 +589,7 @@ func TestActionsPullRequestTargetEvent(t *testing.T) { } func TestActionsSkipCI(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -680,7 +680,7 @@ func TestActionsSkipCI(t *testing.T) { } func TestActionsCreateDeleteRefEvent(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // create the repo @@ -796,7 +796,7 @@ func TestActionsCreateDeleteRefEvent(t *testing.T) { } func TestActionsWorkflowDispatchEvent(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // create the repo @@ -848,3 +848,61 @@ func TestActionsWorkflowDispatchEvent(t *testing.T) { assert.Equal(t, "test", j[0]) }) } + +func TestActionsWorkflowDispatchConcurrencyGroup(t *testing.T) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // create the repo + repo, sha, f := tests.CreateDeclarativeRepo(t, user2, "repo-workflow-dispatch", + []unit_model.Type{unit_model.TypeActions}, nil, + []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/dispatch.yml", + ContentReader: strings.NewReader( + "name: test\n" + + "on: [workflow_dispatch]\n" + + "jobs:\n" + + " test:\n" + + " runs-on: ubuntu-latest\n" + + " steps:\n" + + " - run: echo helloworld\n" + + "concurrency:\n" + + " group: workflow-magic-group\n" + + " cancel-in-progress: true\n", + ), + }, + }, + ) + defer f() + + gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) + require.NoError(t, err) + defer gitRepo.Close() + + workflow, err := actions_service.GetWorkflowFromCommit(gitRepo, "main", "dispatch.yml") + require.NoError(t, err) + assert.Equal(t, "refs/heads/main", workflow.Ref) + assert.Equal(t, sha, workflow.Commit.ID.String()) + + inputGetter := func(key string) string { + return "" + } + + firstRun, _, err := workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2) + require.NoError(t, err) + assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID})) + assert.Equal(t, "workflow-magic-group", firstRun.ConcurrencyGroup) + assert.Equal(t, actions_model.CancelInProgress, firstRun.ConcurrencyType) + + // Dispatch again and verify previous run was cancelled: + secondRun, _, err := workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2) + require.NoError(t, err) + assert.Equal(t, 2, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID})) + assert.Equal(t, "workflow-magic-group", secondRun.ConcurrencyGroup) + assert.Equal(t, actions_model.CancelInProgress, secondRun.ConcurrencyType) + firstRunReload := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: firstRun.ID}) + assert.Equal(t, actions_model.StatusCancelled, firstRunReload.Status) + }) +} diff --git a/tests/integration/actions_view_test.go b/tests/integration/actions_view_test.go index 1a0cdc2ec1..3b124beb65 100644 --- a/tests/integration/actions_view_test.go +++ b/tests/integration/actions_view_test.go @@ -24,7 +24,7 @@ import ( ) func TestActionViewsArtifactDeletion(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // create the repo @@ -152,7 +152,7 @@ func TestActionViewsView(t *testing.T) { re = regexp.MustCompile(pattern) actualClean = re.ReplaceAllString(actualClean, `"time_since_started_html":"_time_"`) - return assert.JSONEq(t, "{\"state\":{\"run\":{\"link\":\"/user5/repo4/actions/runs/187\",\"title\":\"update actions\",\"titleHTML\":\"update actions\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":true,\"jobs\":[{\"id\":192,\"name\":\"job_2\",\"status\":\"success\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"master\",\"link\":\"/user5/repo4/src/branch/master\",\"isDeleted\":false}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Success\",\"steps\":[{\"summary\":\"Set up job\",\"duration\":\"_duration_\",\"status\":\"success\"},{\"summary\":\"Complete job\",\"duration\":\"_duration_\",\"status\":\"success\"}],\"allAttempts\":[{\"number\":3,\"time_since_started_html\":\"_time_\",\"status\":\"running\"},{\"number\":2,\"time_since_started_html\":\"_time_\",\"status\":\"success\"},{\"number\":1,\"time_since_started_html\":\"_time_\",\"status\":\"success\"}]}},\"logs\":{\"stepsLog\":[]}}\n", actualClean) + return assert.JSONEq(t, "{\"state\":{\"run\":{\"preExecutionError\":\"\",\"link\":\"/user5/repo4/actions/runs/187\",\"title\":\"update actions\",\"titleHTML\":\"update actions\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":true,\"jobs\":[{\"id\":192,\"name\":\"job_2\",\"status\":\"success\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"master\",\"link\":\"/user5/repo4/src/branch/master\",\"isDeleted\":false}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Success\",\"steps\":[{\"summary\":\"Set up job\",\"duration\":\"_duration_\",\"status\":\"success\"},{\"summary\":\"Complete job\",\"duration\":\"_duration_\",\"status\":\"success\"}],\"allAttempts\":[{\"number\":3,\"time_since_started_html\":\"_time_\",\"status\":\"running\"},{\"number\":2,\"time_since_started_html\":\"_time_\",\"status\":\"success\"},{\"number\":1,\"time_since_started_html\":\"_time_\",\"status\":\"success\"}]}},\"logs\":{\"stepsLog\":[]}}\n", actualClean) }) htmlDoc.AssertAttrEqual(t, selector, "data-initial-artifacts-response", "{\"artifacts\":[{\"name\":\"multi-file-download\",\"size\":2048,\"status\":\"completed\"}]}\n") } @@ -184,7 +184,7 @@ func TestActionViewsViewAttemptOutOfRange(t *testing.T) { re = regexp.MustCompile(pattern) actualClean = re.ReplaceAllString(actualClean, `"time_since_started_html":"_time_"`) - return assert.JSONEq(t, "{\"state\":{\"run\":{\"link\":\"/user5/repo4/actions/runs/190\",\"title\":\"job output\",\"titleHTML\":\"job output\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":false,\"jobs\":[{\"id\":396,\"name\":\"job_2\",\"status\":\"waiting\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"test\",\"link\":\"/user5/repo4/src/branch/test\",\"isDeleted\":true}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Waiting\",\"steps\":[],\"allAttempts\":null}},\"logs\":{\"stepsLog\":[]}}\n", actualClean) + return assert.JSONEq(t, "{\"state\":{\"run\":{\"preExecutionError\":\"\",\"link\":\"/user5/repo4/actions/runs/190\",\"title\":\"job output\",\"titleHTML\":\"job output\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":false,\"jobs\":[{\"id\":396,\"name\":\"job_2\",\"status\":\"waiting\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"test\",\"link\":\"/user5/repo4/src/branch/test\",\"isDeleted\":true}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Waiting\",\"steps\":[],\"allAttempts\":null}},\"logs\":{\"stepsLog\":[]}}\n", actualClean) }) htmlDoc.AssertAttrEqual(t, selector, "data-initial-artifacts-response", "{\"artifacts\":[]}\n") } diff --git a/tests/integration/activitypub_client_test.go b/tests/integration/activitypub_client_test.go index 67482a7277..9f0d277652 100644 --- a/tests/integration/activitypub_client_test.go +++ b/tests/integration/activitypub_client_test.go @@ -24,7 +24,7 @@ func TestActivityPubClientBodySize(t *testing.T) { defer test.MockVariableValue(&setting.Federation.Enabled, true)() defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) url := u.JoinPath("/api/v1/nodeinfo").String() diff --git a/tests/integration/admin_user_test.go b/tests/integration/admin_user_test.go index f460cf22f5..e2d94b51d2 100644 --- a/tests/integration/admin_user_test.go +++ b/tests/integration/admin_user_test.go @@ -39,6 +39,14 @@ func TestAdminViewUsers(t *testing.T) { // 6th column is the 2FA column. // One user that has TOTP and another user that has WebAuthn. assert.Equal(t, 2, htmlDoc.Find(".admin-setting-content table tbody tr td:nth-child(6) .octicon-check").Length()) + + // account type 5 is for remote users (eg. users from the federation) + req = NewRequest(t, "GET", "/admin/users?status_filter[account_type]=5") + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + + // Only one user (id 42) is a remote user + assert.Equal(t, 1, htmlDoc.Find("table tbody tr").Length()) }) t.Run("Normal user", func(t *testing.T) { @@ -75,6 +83,55 @@ func TestAdminEditUser(t *testing.T) { testSuccessfullEdit(t, user_model.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"}) } +func TestAdminEditUserHideEmail(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user1") + userID := int64(2) // user2 from fixtures + + // Test setting hide_email to false + csrf := GetCSRF(t, session, fmt.Sprintf("/admin/users/%d/edit", userID)) + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/admin/users/%d/edit", userID), map[string]string{ + "_csrf": csrf, + "user_name": "user2", + "login_name": "user2", + "login_type": "0-0", + "email": "user2@example.com", + "hide_email": "false", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) + assert.False(t, user.KeepEmailPrivate) + + // Verify the form now loads with hide_email not checked + req = NewRequest(t, "GET", fmt.Sprintf("/admin/users/%d/edit", userID)) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, `input[name="hide_email"]:not([checked])`, true) + + // Test setting hide_email to true + csrf = GetCSRF(t, session, fmt.Sprintf("/admin/users/%d/edit", userID)) + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/admin/users/%d/edit", userID), map[string]string{ + "_csrf": csrf, + "user_name": "user2", + "login_name": "user2", + "login_type": "0-0", + "email": "user2@example.com", + "hide_email": "true", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) + assert.True(t, user.KeepEmailPrivate) + + // Verify the form loads with hide_email checked + req = NewRequest(t, "GET", fmt.Sprintf("/admin/users/%d/edit", userID)) + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, `input[name="hide_email"][checked]`, true) +} + func testSuccessfullEdit(t *testing.T, formData user_model.User) { makeRequest(t, formData, http.StatusSeeOther) } @@ -148,7 +205,7 @@ func TestSourceId(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &users) assert.Len(t, users, 1) - assert.Equal(t, "the_34-user.with.all.allowedChars", users[0].UserName) + assert.Equal(t, "federated-example.net", users[0].UserName) // Now our new user should be in the list, because we filter by source_id 23 req = NewRequest(t, "GET", "/api/v1/admin/users?limit=1&source_id=23").AddTokenAuth(token) @@ -192,9 +249,9 @@ func TestAdminViewUsersSorted(t *testing.T) { sortType string expectedUsers []string }{ - {0, "alphabetically", []string{"the_34-user.with.all.allowedChars", "user1", "user10", "user11"}}, + {0, "alphabetically", []string{"federated-example.net", "the_34-user.with.all.allowedChars", "user1", "user10"}}, {0, "reversealphabetically", []string{"user9", "user8", "user5", "user40"}}, - {0, "newest", []string{"user40", "user39", "user38", "user37"}}, + {0, "newest", []string{"federated-example.net", "user40", "user39", "user38"}}, {0, "oldest", []string{"user1", "user2", "user4", "user5"}}, {44, "recentupdate", []string{"sorttest1", "sorttest2", "sorttest3", "sorttest4"}}, {44, "leastupdate", []string{"sorttest10", "sorttest9", "sorttest8", "sorttest7"}}, diff --git a/tests/integration/api_activitypub_actor_test.go b/tests/integration/api_activitypub_actor_test.go index 42232bd640..90a759716a 100644 --- a/tests/integration/api_activitypub_actor_test.go +++ b/tests/integration/api_activitypub_actor_test.go @@ -56,7 +56,7 @@ func TestActorNewFromKeyId(t *testing.T) { defer test.MockVariableValue(&setting.Federation.Enabled, true)() defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { ctx, _ := contexttest.MockAPIContext(t, "/api/v1/activitypub/actor") sut, err := federation.NewActorIDFromKeyID(ctx.Base, fmt.Sprintf("%sapi/v1/activitypub/actor#main-key", u)) require.NoError(t, err) diff --git a/tests/integration/api_activitypub_person_inbox_follow_test.go b/tests/integration/api_activitypub_person_inbox_follow_test.go index f171b8951f..9e076ca002 100644 --- a/tests/integration/api_activitypub_person_inbox_follow_test.go +++ b/tests/integration/api_activitypub_person_inbox_follow_test.go @@ -35,7 +35,7 @@ func TestActivityPubPersonInboxFollow(t *testing.T) { federatedSrv := mock.DistantServer(t) defer federatedSrv.Close() - onGiteaRun(t, func(t *testing.T, localUrl *url.URL) { + onApplicationRun(t, func(t *testing.T, localUrl *url.URL) { defer test.MockVariableValue(&setting.AppURL, localUrl.String())() distantURL := federatedSrv.URL diff --git a/tests/integration/api_activitypub_person_inbox_useractivity_test.go b/tests/integration/api_activitypub_person_inbox_useractivity_test.go index 39f08f4d9c..4201fd94bf 100644 --- a/tests/integration/api_activitypub_person_inbox_useractivity_test.go +++ b/tests/integration/api_activitypub_person_inbox_useractivity_test.go @@ -37,7 +37,7 @@ func TestActivityPubPersonInboxNoteToDistant(t *testing.T) { federatedSrv := mock.DistantServer(t) defer federatedSrv.Close() - onGiteaRun(t, func(t *testing.T, localUrl *url.URL) { + onApplicationRun(t, func(t *testing.T, localUrl *url.URL) { defer test.MockVariableValue(&setting.AppURL, localUrl.String())() distantURL := federatedSrv.URL diff --git a/tests/integration/api_activitypub_person_test.go b/tests/integration/api_activitypub_person_test.go index bd21c13612..cbd1f99b23 100644 --- a/tests/integration/api_activitypub_person_test.go +++ b/tests/integration/api_activitypub_person_test.go @@ -32,7 +32,7 @@ func TestActivityPubPerson(t *testing.T) { federatedSrv := mock.DistantServer(t) defer federatedSrv.Close() - onGiteaRun(t, func(t *testing.T, localUrl *url.URL) { + onApplicationRun(t, func(t *testing.T, localUrl *url.URL) { defer test.MockVariableValue(&setting.AppURL, localUrl.String())() localUserID := 2 @@ -90,7 +90,7 @@ func TestActivityPubPersonInbox(t *testing.T) { defer test.MockVariableValue(&setting.Federation.Enabled, true)() defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.AppURL, u.String())() user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) @@ -117,7 +117,7 @@ func TestActivityPubPersonOutbox(t *testing.T) { federatedSrv := mock.DistantServer(t) defer federatedSrv.Close() - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.AppURL, u.String())() user2outboxurl := u.JoinPath("/api/v1/activitypub/user-id/2/outbox").String() diff --git a/tests/integration/api_activitypub_repository_test.go b/tests/integration/api_activitypub_repository_test.go index b4be0407b9..29221bb682 100644 --- a/tests/integration/api_activitypub_repository_test.go +++ b/tests/integration/api_activitypub_repository_test.go @@ -32,7 +32,7 @@ func TestActivityPubRepository(t *testing.T) { federatedSrv := mock.DistantServer(t) defer federatedSrv.Close() - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { repositoryID := 2 localRepository := fmt.Sprintf("%sapi/v1/activitypub/repository-id/%d", u, repositoryID) @@ -76,7 +76,7 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) { federatedSrv := mock.DistantServer(t) defer federatedSrv.Close() - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { repositoryID := 2 timeNow := time.Now().UTC() localRepoInbox := u.JoinPath(fmt.Sprintf("/api/v1/activitypub/repository-id/%d/inbox", repositoryID)).String() @@ -156,7 +156,7 @@ func TestActivityPubRepositoryInboxInvalid(t *testing.T) { defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)() defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { apServerActor := user.NewAPServerActor() repositoryID := 2 localRepo2Inbox := u.JoinPath(fmt.Sprintf("/api/v1/activitypub/repository-id/%d/inbox", repositoryID)).String() diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go index 9351dd9c20..a196cbb758 100644 --- a/tests/integration/api_admin_test.go +++ b/tests/integration/api_admin_test.go @@ -126,7 +126,9 @@ func TestAPIListUsers(t *testing.T) { } } assert.True(t, found) - numberOfUsers := unittest.GetCount(t, &user_model.User{}, "type = 0") + numberOfUsers := unittest.GetCount(t, &user_model.User{}, "type = 0") + + unittest.GetCount(t, &user_model.User{}, "type = 5") + assert.Len(t, users, numberOfUsers) } @@ -222,6 +224,28 @@ func TestAPIEditUser(t *testing.T) { MakeRequest(t, req, http.StatusOK) user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"}) assert.True(t, user2.IsRestricted) + + // Test hide_email functionality + user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"}) + assert.True(t, user2.KeepEmailPrivate) // user2 starts with keep_email_private: true in fixtures + + // Test setting hide_email to false + bFalse := false + req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ + HideEmail: &bFalse, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"}) + assert.False(t, user2.KeepEmailPrivate) + + // Test setting hide_email back to true + bTrue = true + req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ + HideEmail: &bTrue, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"}) + assert.True(t, user2.KeepEmailPrivate) } func TestAPIEditUserWithLoginName(t *testing.T) { diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index d8800217d3..715c5b38cb 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -108,7 +108,7 @@ func TestAPIGetBranch(t *testing.T) { } func TestAPICreateBranch(t *testing.T) { - onGiteaRun(t, testAPICreateBranches) + onApplicationRun(t, testAPICreateBranches) } func testAPICreateBranches(t *testing.T, giteaURL *url.URL) { @@ -189,7 +189,7 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran } func TestAPIUpdateBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, _ *url.URL) { + onApplicationRun(t, func(t *testing.T, _ *url.URL) { t.Run("UpdateBranchWithEmptyRepo", func(t *testing.T) { testAPIUpdateBranch(t, "user10", "repo6", "master", "test", http.StatusNotFound) }) @@ -265,7 +265,7 @@ func TestAPIBranchProtection(t *testing.T) { } func TestAPICreateBranchWithSyncBranches(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { unittest.AssertCount(t, &git_model.Branch{RepoID: 1}, 4) // make a broke repository with no branch on database diff --git a/tests/integration/api_federation_httpsig_test.go b/tests/integration/api_federation_httpsig_test.go index f5c4c78648..a194452dd2 100644 --- a/tests/integration/api_federation_httpsig_test.go +++ b/tests/integration/api_federation_httpsig_test.go @@ -28,7 +28,7 @@ func TestFederationHttpSigValidation(t *testing.T) { defer test.MockVariableValue(&setting.Federation.Enabled, true)() defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { userID := 2 userURL := fmt.Sprintf("%sapi/v1/activitypub/user-id/%d", u, userID) diff --git a/tests/integration/api_issue_templates_test.go b/tests/integration/api_issue_templates_test.go index 3d119c1157..23046543a3 100644 --- a/tests/integration/api_issue_templates_test.go +++ b/tests/integration/api_issue_templates_test.go @@ -20,7 +20,7 @@ import ( ) func TestAPIIssueTemplateList(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) diff --git a/tests/integration/api_packages_cargo_test.go b/tests/integration/api_packages_cargo_test.go index 9e03d9e8a8..febba37b76 100644 --- a/tests/integration/api_packages_cargo_test.go +++ b/tests/integration/api_packages_cargo_test.go @@ -22,7 +22,7 @@ import ( cargo_module "forgejo.org/modules/packages/cargo" "forgejo.org/modules/setting" cargo_router "forgejo.org/routers/api/packages/cargo" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" cargo_service "forgejo.org/services/packages/cargo" "forgejo.org/tests" @@ -31,7 +31,7 @@ import ( ) func TestPackageCargo(t *testing.T) { - onGiteaRun(t, testPackageCargo) + onApplicationRun(t, testPackageCargo) } func testPackageCargo(t *testing.T, _ *neturl.URL) { @@ -388,7 +388,7 @@ func testPackageCargo(t *testing.T, _ *neturl.URL) { } func TestRebuildCargo(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *neturl.URL) { + onApplicationRun(t, func(t *testing.T, u *neturl.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user.Name) unittest.AssertExistsIf(t, false, &repo_model.Repository{OwnerID: user.ID, Name: cargo_service.IndexRepositoryName}) @@ -401,7 +401,7 @@ func TestRebuildCargo(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "error%3DCannot%2Brebuild%252C%2Bno%2Bindex%2Bis%2Binitialized.", flashCookie.Value) unittest.AssertExistsIf(t, false, &repo_model.Repository{OwnerID: user.ID, Name: cargo_service.IndexRepositoryName}) @@ -439,7 +439,7 @@ func TestRebuildCargo(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "success%3DThe%2BCargo%2Bindex%2Bwas%2Bsuccessfully%2Brebuilt.", flashCookie.Value) }) diff --git a/tests/integration/api_packages_debian_test.go b/tests/integration/api_packages_debian_test.go index 67498ec043..b965de9e2c 100644 --- a/tests/integration/api_packages_debian_test.go +++ b/tests/integration/api_packages_debian_test.go @@ -264,4 +264,35 @@ func TestPackageDebian(t *testing.T) { assert.Contains(t, body, "Components: "+strings.Join(components, " ")+"\n") assert.Contains(t, body, "Architectures: "+architectures[1]+"\n") }) + + t.Run("Delete via UI", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Test precondition -- ensure that the packageVersion & packageVersion2 are listed in the index + indexURL := fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distributions[1], components[0], architectures[0]) + req := NewRequest(t, "GET", indexURL) + resp := MakeRequest(t, req, http.StatusOK) + body := resp.Body.String() + require.Contains(t, body, fmt.Sprintf("Version: %s", packageVersion)) + require.Contains(t, body, fmt.Sprintf("Version: %s", packageVersion2)) + + // Perform backend request that simulates the "Delete package" UI option, which is a generic codepath without + // debian-specific package management awareness... + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user.Name) + settingsURL := fmt.Sprintf("/user2/-/packages/debian/%s/%s/settings", packageName, packageVersion) + uiURL := fmt.Sprintf("/user2/-/packages/debian/%s/%s", packageName, packageVersion) + req = NewRequestWithValues(t, "POST", settingsURL, map[string]string{ + "_csrf": GetCSRF(t, session, uiURL), + "action": "delete", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Ensure that the package index has been rebuilt without the deleted package, handled by debianPackageNotifier + req = NewRequest(t, "GET", indexURL) + resp = MakeRequest(t, req, http.StatusOK) + body = resp.Body.String() + assert.NotContains(t, body, fmt.Sprintf("Version: %s", packageVersion)) + require.Contains(t, body, fmt.Sprintf("Version: %s", packageVersion2)) + }) } diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go index 2007b7d6b3..0c616496ff 100644 --- a/tests/integration/api_packages_test.go +++ b/tests/integration/api_packages_test.go @@ -573,6 +573,19 @@ func TestPackageCleanup(t *testing.T) { KeepCount: 2, }, }, + { + Name: "KeepCountGreaterThanTotal", + Versions: []version{ + {Version: "keep", ShouldExist: true}, + {Version: "v1.0", ShouldExist: true}, + {Version: "test-3", ShouldExist: true}, + {Version: "test-4", ShouldExist: true}, + }, + Rule: &packages_model.PackageCleanupRule{ + Enabled: true, + KeepCount: 2000, + }, + }, { Name: "KeepPattern", Versions: []version{ diff --git a/tests/integration/api_private_serv_test.go b/tests/integration/api_private_serv_test.go index 6a62169b40..742d2d80d7 100644 --- a/tests/integration/api_private_serv_test.go +++ b/tests/integration/api_private_serv_test.go @@ -24,7 +24,7 @@ import ( ) func TestAPIPrivateNoServ(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() key, user, err := private.ServNoCommand(ctx, 1) @@ -46,7 +46,7 @@ func TestAPIPrivateNoServ(t *testing.T) { } func TestAPIPrivateServ(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() @@ -161,7 +161,7 @@ func TestAPIPrivateServ(t *testing.T) { } func TestAPIPrivateServAndNoServWithRequiredTwoFactor(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index 301d351238..ef066dee43 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -1,13 +1,15 @@ // Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration import ( + "cmp" "fmt" - "io" "net/http" "net/url" + "slices" "strings" "testing" @@ -31,40 +33,151 @@ import ( func TestAPIViewPulls(t *testing.T) { defer tests.PrepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all", owner.Name, repo.Name). - AddTokenAuth(ctx.Token) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all", repo.OwnerName, repo.Name).AddTokenAuth(ctx.Token) resp := ctx.Session.MakeRequest(t, req, http.StatusOK) var pulls []*api.PullRequest DecodeJSON(t, resp, &pulls) - expectedLen := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true)) - assert.Len(t, pulls, expectedLen) + if assert.Len(t, pulls, 3) { + slices.SortFunc(pulls, func(a, b *api.PullRequest) int { + return cmp.Compare(a.ID, b.ID) + }) - pull := pulls[0] - if assert.EqualValues(t, 5, pull.ID) { - resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK) - _, err := io.ReadAll(resp.Body) - require.NoError(t, err) - // TODO: use diff to generate stats to test against + assert.EqualValues(t, 1, pulls[0].ID) + assert.EqualValues(t, 2, pulls[0].Index) - t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID), - doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) { - if assert.Len(t, files, 1) { - assert.Equal(t, "File-WoW", files[0].Filename) - assert.Empty(t, files[0].PreviousFilename) - assert.Equal(t, 1, files[0].Additions) - assert.Equal(t, 1, files[0].Changes) - assert.Equal(t, 0, files[0].Deletions) - assert.Equal(t, "added", files[0].Status) - } - })) + assert.EqualValues(t, 2, pulls[1].ID) + assert.EqualValues(t, 3, pulls[1].Index) + + assert.EqualValues(t, 5, pulls[2].ID) + assert.EqualValues(t, 5, pulls[2].Index) } } +func TestAPIPullsFiles(t *testing.T) { + defer tests.PrepareTestEnv(t)() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository) + + t.Run("Pull 1", func(t *testing.T) { + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/2/files", repo.OwnerName, repo.Name).AddTokenAuth(ctx.Token) + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + + var changedFiles []*api.ChangedFile + DecodeJSON(t, resp, &changedFiles) + + assert.Empty(t, changedFiles) + assert.Equal(t, "0", resp.Header().Get("X-Total-Count")) + assert.Equal(t, "false", resp.Header().Get("X-HasMore")) + }) + + t.Run("Pull 2", func(t *testing.T) { + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/3/files", repo.OwnerName, repo.Name).AddTokenAuth(ctx.Token) + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + + var changedFiles []*api.ChangedFile + DecodeJSON(t, resp, &changedFiles) + + if assert.Len(t, changedFiles, 2) { + assert.Equal(t, "2", resp.Header().Get("X-Total-Count")) + assert.Equal(t, "false", resp.Header().Get("X-HasMore")) + + assert.Equal(t, "3", changedFiles[0].Filename) + assert.Empty(t, changedFiles[0].PreviousFilename) + assert.Equal(t, "added", changedFiles[0].Status) + assert.Equal(t, 1, changedFiles[0].Changes) + assert.Equal(t, 1, changedFiles[0].Additions) + assert.Equal(t, 0, changedFiles[0].Deletions) + assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/contents/3?ref=5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", changedFiles[0].ContentsURL) + assert.Equal(t, setting.AppURL+"user2/repo1/raw/commit/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd/3", changedFiles[0].RawURL) + assert.Equal(t, setting.AppURL+"user2/repo1/src/commit/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd/3", changedFiles[0].HTMLURL) + + assert.Equal(t, "iso-8859-1.txt", changedFiles[1].Filename) + assert.Empty(t, changedFiles[1].PreviousFilename) + assert.Equal(t, "added", changedFiles[1].Status) + assert.Equal(t, 10, changedFiles[1].Changes) + assert.Equal(t, 10, changedFiles[1].Additions) + assert.Equal(t, 0, changedFiles[1].Deletions) + assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/contents/iso-8859-1.txt?ref=5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", changedFiles[1].ContentsURL) + assert.Equal(t, setting.AppURL+"user2/repo1/raw/commit/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd/iso-8859-1.txt", changedFiles[1].RawURL) + assert.Equal(t, setting.AppURL+"user2/repo1/src/commit/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd/iso-8859-1.txt", changedFiles[1].HTMLURL) + } + }) + + t.Run("Pull 5", func(t *testing.T) { + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/5/files", repo.OwnerName, repo.Name).AddTokenAuth(ctx.Token) + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + + var changedFiles []*api.ChangedFile + DecodeJSON(t, resp, &changedFiles) + + if assert.Len(t, changedFiles, 1) { + assert.Equal(t, "1", resp.Header().Get("X-Total-Count")) + assert.Equal(t, "false", resp.Header().Get("X-HasMore")) + + assert.Equal(t, "File-WoW", changedFiles[0].Filename) + assert.Empty(t, changedFiles[0].PreviousFilename) + assert.Equal(t, "added", changedFiles[0].Status) + assert.Equal(t, 1, changedFiles[0].Changes) + assert.Equal(t, 1, changedFiles[0].Additions) + assert.Equal(t, 0, changedFiles[0].Deletions) + assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/contents/File-WoW?ref=62fb502a7172d4453f0322a2cc85bddffa57f07a", changedFiles[0].ContentsURL) + assert.Equal(t, setting.AppURL+"user2/repo1/raw/commit/62fb502a7172d4453f0322a2cc85bddffa57f07a/File-WoW", changedFiles[0].RawURL) + assert.Equal(t, setting.AppURL+"user2/repo1/src/commit/62fb502a7172d4453f0322a2cc85bddffa57f07a/File-WoW", changedFiles[0].HTMLURL) + } + }) + + t.Run("Pull 7", func(t *testing.T) { + req := NewRequest(t, "GET", "/api/v1/repos/user2/commitsonpr/pulls/1/files?limit=3").AddTokenAuth(ctx.Token) + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + + var changedFiles []*api.ChangedFile + DecodeJSON(t, resp, &changedFiles) + + if assert.Len(t, changedFiles, 3) { + assert.Equal(t, "10", resp.Header().Get("X-Total-Count")) + assert.Equal(t, "true", resp.Header().Get("X-HasMore")) + assert.Equal(t, "1", resp.Header().Get("X-Page")) + assert.Equal(t, "3", resp.Header().Get("X-PerPage")) + assert.Equal(t, "4", resp.Header().Get("X-PageCount")) + + assert.Equal(t, "test1.txt", changedFiles[0].Filename) + assert.Empty(t, changedFiles[0].PreviousFilename) + assert.Equal(t, "added", changedFiles[0].Status) + assert.Equal(t, 1, changedFiles[0].Changes) + assert.Equal(t, 1, changedFiles[0].Additions) + assert.Equal(t, 0, changedFiles[0].Deletions) + assert.Equal(t, setting.AppURL+"api/v1/repos/user2/commitsonpr/contents/test1.txt?ref=9b93963cf6de4dc33f915bb67f192d099c301f43", changedFiles[0].ContentsURL) + assert.Equal(t, setting.AppURL+"user2/commitsonpr/raw/commit/9b93963cf6de4dc33f915bb67f192d099c301f43/test1.txt", changedFiles[0].RawURL) + assert.Equal(t, setting.AppURL+"user2/commitsonpr/src/commit/9b93963cf6de4dc33f915bb67f192d099c301f43/test1.txt", changedFiles[0].HTMLURL) + + assert.Equal(t, "test10.txt", changedFiles[1].Filename) + assert.Empty(t, changedFiles[1].PreviousFilename) + assert.Equal(t, "added", changedFiles[1].Status) + assert.Equal(t, 1, changedFiles[1].Changes) + assert.Equal(t, 1, changedFiles[1].Additions) + assert.Equal(t, 0, changedFiles[1].Deletions) + assert.Equal(t, setting.AppURL+"api/v1/repos/user2/commitsonpr/contents/test10.txt?ref=9b93963cf6de4dc33f915bb67f192d099c301f43", changedFiles[1].ContentsURL) + assert.Equal(t, setting.AppURL+"user2/commitsonpr/raw/commit/9b93963cf6de4dc33f915bb67f192d099c301f43/test10.txt", changedFiles[1].RawURL) + assert.Equal(t, setting.AppURL+"user2/commitsonpr/src/commit/9b93963cf6de4dc33f915bb67f192d099c301f43/test10.txt", changedFiles[1].HTMLURL) + + assert.Equal(t, "test2.txt", changedFiles[2].Filename) + assert.Empty(t, changedFiles[2].PreviousFilename) + assert.Equal(t, "added", changedFiles[2].Status) + assert.Equal(t, 1, changedFiles[2].Changes) + assert.Equal(t, 1, changedFiles[2].Additions) + assert.Equal(t, 0, changedFiles[2].Deletions) + assert.Equal(t, setting.AppURL+"api/v1/repos/user2/commitsonpr/contents/test2.txt?ref=9b93963cf6de4dc33f915bb67f192d099c301f43", changedFiles[2].ContentsURL) + assert.Equal(t, setting.AppURL+"user2/commitsonpr/raw/commit/9b93963cf6de4dc33f915bb67f192d099c301f43/test2.txt", changedFiles[2].RawURL) + assert.Equal(t, setting.AppURL+"user2/commitsonpr/src/commit/9b93963cf6de4dc33f915bb67f192d099c301f43/test2.txt", changedFiles[2].HTMLURL) + } + }) +} + func TestAPIViewPullsByBaseHead(t *testing.T) { defer tests.PrepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -126,8 +239,13 @@ func TestAPICreatePullSuccess(t *testing.T) { Base: "master", Title: "create a failure pr", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusCreated) + res := MakeRequest(t, req, http.StatusCreated) MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail + + pull := new(api.PullRequest) + DecodeJSON(t, res, pull) + + assert.Equal(t, "65f1bf27bc3bf70f64657658635e66094edbcb4d", pull.MergeBase) } func TestAPICreatePullSameRepoSuccess(t *testing.T) { @@ -143,8 +261,13 @@ func TestAPICreatePullSameRepoSuccess(t *testing.T) { Base: "master", Title: "successfully create a PR between branches of the same repository", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusCreated) + res := MakeRequest(t, req, http.StatusCreated) MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail + + pull := new(api.PullRequest) + DecodeJSON(t, res, pull) + + assert.Equal(t, "65f1bf27bc3bf70f64657658635e66094edbcb4d", pull.MergeBase) } func TestAPICreatePullWithFieldsSuccess(t *testing.T) { @@ -295,26 +418,8 @@ func TestAPIForkDifferentName(t *testing.T) { MakeRequest(t, req, http.StatusCreated) } -func doAPIGetPullFiles(ctx APITestContext, pr *api.PullRequest, callback func(*testing.T, []*api.ChangedFile)) func(*testing.T) { - return func(t *testing.T) { - req := NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/files", ctx.Username, ctx.Reponame, pr.Index)). - AddTokenAuth(ctx.Token) - if ctx.ExpectedCode == 0 { - ctx.ExpectedCode = http.StatusOK - } - resp := ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) - - files := make([]*api.ChangedFile, 0, 1) - DecodeJSON(t, resp, &files) - - if callback != nil { - callback(t, files) - } - } -} - func TestAPIPullDeleteBranchPerms(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { user2Session := loginUser(t, "user2") user4Session := loginUser(t, "user4") testRepoFork(t, user4Session, "user2", "repo1", "user4", "repo1") diff --git a/tests/integration/api_push_mirror_test.go b/tests/integration/api_push_mirror_test.go index 48976fd5eb..14af184048 100644 --- a/tests/integration/api_push_mirror_test.go +++ b/tests/integration/api_push_mirror_test.go @@ -40,7 +40,7 @@ import ( ) func TestAPIPushMirror(t *testing.T) { - onGiteaRun(t, testAPIPushMirror) + onApplicationRun(t, testAPIPushMirror) } func testAPIPushMirror(t *testing.T, u *url.URL) { @@ -144,7 +144,7 @@ func testAPIPushMirror(t *testing.T, u *url.URL) { } func TestAPIPushMirrorBranchFilter(t *testing.T) { - onGiteaRun(t, testAPIPushMirrorBranchFilter) + onApplicationRun(t, testAPIPushMirrorBranchFilter) } func testAPIPushMirrorBranchFilter(t *testing.T, u *url.URL) { @@ -302,7 +302,7 @@ func TestAPIPushMirrorSSH(t *testing.T) { t.Skip("SSH executable not present") } - onGiteaRun(t, func(t *testing.T, _ *url.URL) { + onApplicationRun(t, func(t *testing.T, _ *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.Mirror.Enabled, true)() defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())() diff --git a/tests/integration/api_quota_use_test.go b/tests/integration/api_quota_use_test.go index 1e3d41428a..1d1cf6062c 100644 --- a/tests/integration/api_quota_use_test.go +++ b/tests/integration/api_quota_use_test.go @@ -288,7 +288,7 @@ func prepareQuotaEnv(t *testing.T, username string) *quotaEnv { } func TestAPIQuotaUserCleanSlate(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Quota.Enabled, true)() defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() @@ -308,13 +308,13 @@ func TestAPIQuotaUserCleanSlate(t *testing.T) { } func TestAPIQuotaEnforcement(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { testAPIQuotaEnforcement(t) }) } func TestAPIQuotaCountsTowardsCorrectUser(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := prepareQuotaEnv(t, "quota-correct-user-test") defer env.Cleanup() env.SetupWithSingleQuotaRule(t) @@ -350,7 +350,7 @@ func TestAPIQuotaCountsTowardsCorrectUser(t *testing.T) { } func TestAPIQuotaError(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := prepareQuotaEnv(t, "quota-enforcement") defer env.Cleanup() env.SetupWithSingleQuotaRule(t) @@ -1288,7 +1288,7 @@ func testAPIQuotaEnforcement(t *testing.T) { } func TestAPIQuotaOrgQuotaQuery(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := prepareQuotaEnv(t, "quota-enforcement") defer env.Cleanup() @@ -1315,7 +1315,7 @@ func TestAPIQuotaOrgQuotaQuery(t *testing.T) { } func TestAPIQuotaUserBasics(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := prepareQuotaEnv(t, "quota-enforcement") defer env.Cleanup() diff --git a/tests/integration/api_repo_actions_test.go b/tests/integration/api_repo_actions_test.go index 348ff45ed9..1875f3269e 100644 --- a/tests/integration/api_repo_actions_test.go +++ b/tests/integration/api_repo_actions_test.go @@ -5,6 +5,7 @@ package integration import ( "fmt" + "io" "net/http" "net/url" "strings" @@ -21,6 +22,7 @@ import ( "forgejo.org/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestActionsAPISearchActionJobs_RepoRunner(t *testing.T) { @@ -48,7 +50,7 @@ func TestActionsAPISearchActionJobs_RepoRunner(t *testing.T) { } func TestActionsAPIWorkflowDispatchReturnInfo(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { workflowName := "dispatch.yml" user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository) @@ -99,6 +101,24 @@ jobs: assert.NotZero(t, run.ID) assert.NotZero(t, run.RunNumber) assert.Len(t, run.Jobs, 2) + + req = NewRequestWithJSON( + t, + http.MethodPost, + fmt.Sprintf( + "/api/v1/repos/%s/%s/actions/workflows/%s/dispatches", + repo.OwnerName, repo.Name, workflowName, + ), + &api.DispatchWorkflowOption{ + Ref: repo.DefaultBranch, + ReturnRunInfo: false, + }, + ) + req.AddTokenAuth(token) + res = MakeRequest(t, req, http.StatusNoContent) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Empty(t, body) // 204 No Content doesn't support a body, so should be empty }) } diff --git a/tests/integration/api_repo_branch_test.go b/tests/integration/api_repo_branch_test.go index 6ffe40ff44..a80ced6f12 100644 --- a/tests/integration/api_repo_branch_test.go +++ b/tests/integration/api_repo_branch_test.go @@ -25,7 +25,7 @@ import ( ) func TestAPIRepoBranchesPlain(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) session := loginUser(t, user1.LowerName) diff --git a/tests/integration/api_repo_collaborator_test.go b/tests/integration/api_repo_collaborator_test.go index 61f4f578d7..e7a19375a4 100644 --- a/tests/integration/api_repo_collaborator_test.go +++ b/tests/integration/api_repo_collaborator_test.go @@ -19,7 +19,7 @@ import ( ) func TestAPIRepoCollaboratorPermission(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) repo2Owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo2.OwnerID}) diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go index 1724924fdc..42f997eb7d 100644 --- a/tests/integration/api_repo_compare_test.go +++ b/tests/integration/api_repo_compare_test.go @@ -27,7 +27,7 @@ func TestAPICompareCommits(t *testing.T) { } func testAPICompareCommits(t *testing.T, objectFormat git.ObjectFormat) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { newBranchAndFile := func(ctx APITestContext, user *user_model.User, branch, filename string) func(*testing.T) { return func(t *testing.T) { doAPICreateFile(ctx, filename, &api.CreateFileOptions{ diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go index 4916ef97ef..536246d42e 100644 --- a/tests/integration/api_repo_file_create_test.go +++ b/tests/integration/api_repo_file_create_test.go @@ -116,7 +116,7 @@ func getExpectedFileResponseForCreate(repoFullName, commitID, treePath, latestCo } func BenchmarkAPICreateFileSmall(b *testing.B) { - onGiteaRun(b, func(b *testing.B, u *url.URL) { + onApplicationRun(b, func(b *testing.B, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(b, &user_model.User{ID: 2}) // owner of the repo1 & repo16 repo1 := unittest.AssertExistsAndLoadBean(b, &repo_model.Repository{ID: 1}) // public repo @@ -131,7 +131,7 @@ func BenchmarkAPICreateFileSmall(b *testing.B) { func BenchmarkAPICreateFileMedium(b *testing.B) { data := make([]byte, 10*1024*1024) - onGiteaRun(b, func(b *testing.B, u *url.URL) { + onApplicationRun(b, func(b *testing.B, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(b, &user_model.User{ID: 2}) // owner of the repo1 & repo16 repo1 := unittest.AssertExistsAndLoadBean(b, &repo_model.Repository{ID: 1}) // public repo @@ -145,7 +145,7 @@ func BenchmarkAPICreateFileMedium(b *testing.B) { } func TestAPICreateFile(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16 org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos diff --git a/tests/integration/api_repo_file_delete_test.go b/tests/integration/api_repo_file_delete_test.go index 428ef37e34..e50f170e74 100644 --- a/tests/integration/api_repo_file_delete_test.go +++ b/tests/integration/api_repo_file_delete_test.go @@ -38,7 +38,7 @@ func getDeleteFileOptions() *api.DeleteFileOptions { } func TestAPIDeleteFile(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16 org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos diff --git a/tests/integration/api_repo_file_get_test.go b/tests/integration/api_repo_file_get_test.go index ab82e5cf15..ba3862569b 100644 --- a/tests/integration/api_repo_file_get_test.go +++ b/tests/integration/api_repo_file_get_test.go @@ -17,7 +17,7 @@ import ( ) func TestAPIGetRawFileOrLFS(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { t.Run("Normal raw file", func(t *testing.T) { defer tests.PrintCurrentTest(t)() diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go index 878d865aff..1890544035 100644 --- a/tests/integration/api_repo_file_update_test.go +++ b/tests/integration/api_repo_file_update_test.go @@ -107,7 +107,7 @@ func getExpectedFileResponseForUpdate(commitID, treePath, lastCommitSHA string) } func TestAPIUpdateFile(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16 org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos diff --git a/tests/integration/api_repo_files_change_test.go b/tests/integration/api_repo_files_change_test.go index 6b1edd047b..341464f1de 100644 --- a/tests/integration/api_repo_files_change_test.go +++ b/tests/integration/api_repo_files_change_test.go @@ -60,7 +60,7 @@ func getChangeFilesOptions() *api.ChangeFilesOptions { } func TestAPIChangeFiles(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16 org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos diff --git a/tests/integration/api_repo_get_contents_list_test.go b/tests/integration/api_repo_get_contents_list_test.go index 7d010ffdf1..f86885913c 100644 --- a/tests/integration/api_repo_get_contents_list_test.go +++ b/tests/integration/api_repo_get_contents_list_test.go @@ -54,7 +54,7 @@ func getExpectedContentsListResponseForContents(ref, refType, lastCommitSHA stri } func TestAPIGetContentsList(t *testing.T) { - onGiteaRun(t, testAPIGetContentsList) + onApplicationRun(t, testAPIGetContentsList) } func testAPIGetContentsList(t *testing.T, u *url.URL) { diff --git a/tests/integration/api_repo_get_contents_test.go b/tests/integration/api_repo_get_contents_test.go index 47ac4cfc03..52c83cdab6 100644 --- a/tests/integration/api_repo_get_contents_test.go +++ b/tests/integration/api_repo_get_contents_test.go @@ -56,7 +56,7 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) } func TestAPIGetContents(t *testing.T) { - onGiteaRun(t, testAPIGetContents) + onApplicationRun(t, testAPIGetContents) } func testAPIGetContents(t *testing.T, u *url.URL) { @@ -171,7 +171,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) { } func TestAPIGetContentsRefFormats(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { file := "README.md" sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" content := "# repo1\n\nDescription for repo1" diff --git a/tests/integration/api_repo_git_notes_test.go b/tests/integration/api_repo_git_notes_test.go index dfafec7135..2e55ac25b1 100644 --- a/tests/integration/api_repo_git_notes_test.go +++ b/tests/integration/api_repo_git_notes_test.go @@ -19,7 +19,7 @@ import ( ) func TestAPIReposGetGitNotes(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // Login as User2. session := loginUser(t, user.Name) @@ -48,7 +48,7 @@ func TestAPIReposGetGitNotes(t *testing.T) { } func TestAPIReposSetGitNotes(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -76,7 +76,7 @@ func TestAPIReposSetGitNotes(t *testing.T) { } func TestAPIReposDeleteGitNotes(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) diff --git a/tests/integration/api_repo_languages_test.go b/tests/integration/api_repo_languages_test.go index d9a7f38db1..ef80037573 100644 --- a/tests/integration/api_repo_languages_test.go +++ b/tests/integration/api_repo_languages_test.go @@ -12,7 +12,7 @@ import ( ) func TestRepoLanguages(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") // Request editor page diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index e81f4307ee..1c17d5e04f 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -402,7 +402,7 @@ func TestAPIRepoMigrate(t *testing.T) { } func TestAPIRepoMigrateConflict(t *testing.T) { - onGiteaRun(t, testAPIRepoMigrateConflict) + onApplicationRun(t, testAPIRepoMigrateConflict) } func testAPIRepoMigrateConflict(t *testing.T, u *url.URL) { @@ -485,7 +485,7 @@ func TestAPIOrgRepoCreate(t *testing.T) { } func TestAPIRepoCreateConflict(t *testing.T) { - onGiteaRun(t, testAPIRepoCreateConflict) + onApplicationRun(t, testAPIRepoCreateConflict) } func testAPIRepoCreateConflict(t *testing.T, u *url.URL) { diff --git a/tests/integration/api_wiki_test.go b/tests/integration/api_wiki_test.go index 1720587dd4..d1a4102060 100644 --- a/tests/integration/api_wiki_test.go +++ b/tests/integration/api_wiki_test.go @@ -432,7 +432,7 @@ func TestAPIListPageRevisions(t *testing.T) { } func TestAPIWikiNonMasterBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, _ *url.URL) { + onApplicationRun(t, func(t *testing.T, _ *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{ WikiBranch: optional.Some("main"), diff --git a/tests/integration/archived_labels_display_test.go b/tests/integration/archived_labels_display_test.go index c9748f81d6..4c9c605a10 100644 --- a/tests/integration/archived_labels_display_test.go +++ b/tests/integration/archived_labels_display_test.go @@ -14,7 +14,7 @@ import ( ) func TestArchivedLabelVisualProperties(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") // Create labels diff --git a/tests/integration/benchmarks_test.go b/tests/integration/benchmarks_test.go index 40875e0c7d..1aa3b821c2 100644 --- a/tests/integration/benchmarks_test.go +++ b/tests/integration/benchmarks_test.go @@ -24,7 +24,7 @@ func StringWithCharset(length int, charset string) string { } func BenchmarkRepoBranchCommit(b *testing.B) { - onGiteaRun(b, func(b *testing.B, u *url.URL) { + onApplicationRun(b, func(b *testing.B, u *url.URL) { samples := []int64{1, 2, 3} b.ResetTimer() diff --git a/tests/integration/branches_test.go b/tests/integration/branches_test.go index c599106866..3b3f423391 100644 --- a/tests/integration/branches_test.go +++ b/tests/integration/branches_test.go @@ -12,13 +12,13 @@ import ( git_model "forgejo.org/models/git" repo_model "forgejo.org/models/repo" "forgejo.org/models/unittest" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" "github.com/stretchr/testify/assert" ) func TestBranchActions(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) branch3 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 3, RepoID: repo1.ID}) @@ -35,7 +35,7 @@ func TestBranchActions(t *testing.T) { "_csrf": GetCSRF(t, session, branchesLink), }) session.MakeRequest(t, req, http.StatusOK) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522branch2%2522%2Bhas%2Bbeen%2Bdeleted.") @@ -48,7 +48,7 @@ func TestBranchActions(t *testing.T) { "_csrf": GetCSRF(t, session, branchesLink), }) session.MakeRequest(t, req, http.StatusOK) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522branch2%2522%2Bhas%2Bbeen%2Brestored") diff --git a/tests/integration/cmd_admin_test.go b/tests/integration/cmd_admin_test.go index c06f7f7213..ea07b9bf3e 100644 --- a/tests/integration/cmd_admin_test.go +++ b/tests/integration/cmd_admin_test.go @@ -20,7 +20,7 @@ import ( ) func Test_Cmd_AdminUser(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { for i, testCase := range []struct { name string options []string @@ -76,7 +76,7 @@ func Test_Cmd_AdminUser(t *testing.T) { } func Test_Cmd_AdminFirstUser(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { for _, testCase := range []struct { name string options []string @@ -133,7 +133,7 @@ func Test_Cmd_AdminFirstUser(t *testing.T) { }, } { t.Run(testCase.name, func(t *testing.T) { - db.GetEngine(db.DefaultContext).Exec("DELETE FROM `user`") + db.TruncateBeansCascade(db.DefaultContext, user_model.User{}) db.GetEngine(db.DefaultContext).Exec("DELETE FROM `email_address`") assert.Equal(t, int64(0), user_model.CountUsers(db.DefaultContext, nil)) name := "testuser" @@ -152,7 +152,7 @@ func Test_Cmd_AdminFirstUser(t *testing.T) { } func Test_Cmd_AdminUserResetMFA(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { name := "testuser" options := []string{"user", "create", "--username", name, "--password", "password", "--email", name + "@example.com"} diff --git a/tests/integration/cmd_forgejo_actions_test.go b/tests/integration/cmd_forgejo_actions_test.go index 9d7db42c2e..653ff65a9d 100644 --- a/tests/integration/cmd_forgejo_actions_test.go +++ b/tests/integration/cmd_forgejo_actions_test.go @@ -21,7 +21,7 @@ import ( ) func TestActions_CmdForgejo_Actions(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { token, err := runMainApp("forgejo-cli", "actions", "generate-runner-token") require.NoError(t, err) assert.Len(t, token, 40) diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go index 55d97a3a1d..edc44056b3 100644 --- a/tests/integration/cmd_keys_test.go +++ b/tests/integration/cmd_keys_test.go @@ -17,7 +17,7 @@ import ( ) func Test_CmdKeys(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { tests := []struct { name string args []string diff --git a/tests/integration/codeowner_test.go b/tests/integration/codeowner_test.go index 51468ffe09..632f07b647 100644 --- a/tests/integration/codeowner_test.go +++ b/tests/integration/codeowner_test.go @@ -204,7 +204,7 @@ type CodeownerTest struct { } func TestCodeOwner(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { tests := []CodeownerTest{ {Name: "root", Path: "CODEOWNERS"}, {Name: "docs", Path: "docs/CODEOWNERS"}, diff --git a/tests/integration/comment_roles_test.go b/tests/integration/comment_roles_test.go index 63b5d5f7cc..d5346e444e 100644 --- a/tests/integration/comment_roles_test.go +++ b/tests/integration/comment_roles_test.go @@ -29,7 +29,7 @@ func TestCommentRoles(t *testing.T) { newContributorTooltip := locale.TrString("repo.issues.role.first_time_contributor_helper") // Test pulls - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { sessionUser1 := loginUser(t, "user1") sessionUser2 := loginUser(t, "user2") sessionUser11 := loginUser(t, "user11") @@ -109,7 +109,7 @@ func TestCommentRoles(t *testing.T) { }) // Test issues - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { sessionUser1 := loginUser(t, "user1") sessionUser2 := loginUser(t, "user2") sessionUser5 := loginUser(t, "user5") diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index 581aa67659..396c1c22ad 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -242,7 +242,7 @@ func TestCompareBranches(t *testing.T) { } func TestCompareWithPRsDisabled(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testCreateBranch(t, session, "user1", "repo1", "branch/master", "recent-push", http.StatusSeeOther) @@ -303,7 +303,7 @@ func TestCompareWithPRsDisabled(t *testing.T) { } func TestCompareCrossRepo(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1-copy") testCreateBranch(t, session, "user1", "repo1-copy", "branch/master", "recent-push", http.StatusSeeOther) @@ -332,7 +332,7 @@ func TestCompareCrossRepo(t *testing.T) { } func TestCompareCodeExpand(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // Create a new repository, with a file that has many lines @@ -405,7 +405,7 @@ func TestCompareCodeExpand(t *testing.T) { } func TestCompareSignedIn(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { // Setup the test with a connected user session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") diff --git a/tests/integration/dump_restore_test.go b/tests/integration/dump_restore_test.go index daf95eb631..592af6ad87 100644 --- a/tests/integration/dump_restore_test.go +++ b/tests/integration/dump_restore_test.go @@ -28,7 +28,7 @@ import ( ) func TestDumpRestore(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { AllowLocalNetworks := setting.Migrations.AllowLocalNetworks setting.Migrations.AllowLocalNetworks = true AppVer := setting.AppVer diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index b6faf8a118..7909560e86 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -21,7 +21,7 @@ import ( "forgejo.org/modules/git" "forgejo.org/modules/json" "forgejo.org/modules/translation" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -29,7 +29,7 @@ import ( ) func TestCreateFileOnProtectedBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") csrf := GetCSRF(t, session, "/user2/repo1/settings/branches") @@ -41,7 +41,7 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) // Check if master branch has been locked successfully - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522master%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) @@ -82,7 +82,7 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { assert.Equal(t, "/user2/repo1/settings/branches", res["redirect"]) // Check if master branch has been locked successfully - flashCookie = session.GetCookie(gitea_context.CookieNameFlash) + flashCookie = session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "error%3DRemoving%2Bbranch%2Bprotection%2Brule%2B%25221%2522%2Bfailed.", flashCookie.Value) }) @@ -150,14 +150,14 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra } func TestEditFile(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") testEditFile(t, session, "user2", "repo1", "master", "README.md", "Hello, World (Edited)\n") }) } func TestEditFileToNewBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") }) @@ -177,7 +177,7 @@ func TestEditorAddTranslation(t *testing.T) { } func TestCommitMail(t *testing.T) { - onGiteaRun(t, func(t *testing.T, _ *url.URL) { + onApplicationRun(t, func(t *testing.T, _ *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // Require that the user has KeepEmailPrivate enabled, because it needs // to be tested that even with this setting enabled, it will use the @@ -407,22 +407,26 @@ func TestCommitMail(t *testing.T) { file1UUID := uploadFile(t, "upload_file_1", "Uploaded a file!") file2UUID := uploadFile(t, "upload_file_2", "Uploaded another file!") + file1UUIDFullpathKey := fmt.Sprintf("files_fullpath[%s]", file1UUID) + file2UUIDFullpathKey := fmt.Sprintf("files_fullpath[%s]", file2UUID) assertCase(t, caseOpts{ fileName: "upload_file_1", link: "user2/repo1/_upload/master", skipLastCommit: true, base: map[string]string{ - "commit_choice": "direct", - "files": file1UUID, + "commit_choice": "direct", + "files": file1UUID, + file1UUIDFullpathKey: "upload_file_1", }, }, caseOpts{ fileName: "upload_file_2", link: "user2/repo1/_upload/master", skipLastCommit: true, base: map[string]string{ - "commit_choice": "direct", - "files": file2UUID, + "commit_choice": "direct", + "files": file2UUID, + file2UUIDFullpathKey: "upload_file_2", }, }, ) diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go index 151f9450bd..ba5db5ded4 100644 --- a/tests/integration/empty_repo_test.go +++ b/tests/integration/empty_repo_test.go @@ -6,6 +6,7 @@ package integration import ( "bytes" "encoding/base64" + "fmt" "io" "mime/multipart" "net/http" @@ -88,11 +89,12 @@ func TestEmptyRepoUploadFile(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) respMap := map[string]string{} require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &respMap)) - + filesFullpathKey := fmt.Sprintf("files_fullpath[%s]", respMap["uuid"]) req = NewRequestWithValues(t, "POST", "/user30/empty/_upload/"+setting.Repository.DefaultBranch, map[string]string{ "_csrf": GetCSRF(t, session, "/user/settings"), "commit_choice": "direct", "files": respMap["uuid"], + filesFullpathKey: "uploaded-file.txt", "tree_path": "", "commit_mail_id": "-1", }) diff --git a/tests/integration/explore_org_test.go b/tests/integration/explore_org_test.go index 111fd2dda7..aeefefa07f 100644 --- a/tests/integration/explore_org_test.go +++ b/tests/integration/explore_org_test.go @@ -1,4 +1,5 @@ // Copyright 2024 The Gitea Authors. All rights reserved. +// Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -31,7 +32,7 @@ func TestExploreOrg(t *testing.T) { req := NewRequest(t, "GET", "/explore/organizations?sort="+c.sortOrder) resp := MakeRequest(t, req, http.StatusOK) h := NewHTMLParser(t, resp.Body) - href, _ := h.Find(`.ui.dropdown .menu a.active.item[href^="?sort="]`).Attr("href") + href, _ := h.Find(`.list-header details.dropdown > ul > li > a.active[href^="?sort="]`).Attr("href") assert.Equal(t, c.expected, href) } diff --git a/tests/integration/explore_user_test.go b/tests/integration/explore_user_test.go index 689e623e69..2d46bf2d6e 100644 --- a/tests/integration/explore_user_test.go +++ b/tests/integration/explore_user_test.go @@ -1,4 +1,5 @@ // Copyright 2024 The Gitea Authors. All rights reserved. +// Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -31,7 +32,7 @@ func TestExploreUser(t *testing.T) { req := NewRequest(t, "GET", "/explore/users?sort="+c.sortOrder) resp := MakeRequest(t, req, http.StatusOK) h := NewHTMLParser(t, resp.Body) - href, _ := h.Find(`.ui.dropdown .menu a.active.item[href^="?sort="]`).Attr("href") + href, _ := h.Find(`.list-header details.dropdown > ul > li > a.active[href^="?sort="]`).Attr("href") assert.Equal(t, c.expected, href) } diff --git a/tests/integration/fixtures/TestActionConcurrencyGroupQueue/action_run.yml b/tests/integration/fixtures/TestActionConcurrencyGroupQueue/action_run.yml new file mode 100644 index 0000000000..f2cc456b8d --- /dev/null +++ b/tests/integration/fixtures/TestActionConcurrencyGroupQueue/action_run.yml @@ -0,0 +1,21 @@ +- + id: 500 + index: 1 + status: 5 # StatusWaiting + repo_id: 4 + concurrency_group: abc123 + concurrency_type: 1 # QueueBehind +- + id: 501 + index: 2 + status: 5 # StatusWaiting + repo_id: 4 + concurrency_group: abc123 + concurrency_type: 1 # QueueBehind +- + id: 502 + index: 3 + status: 5 # StatusWaiting + repo_id: 4 + concurrency_group: abc123 + concurrency_type: 1 # QueueBehind diff --git a/tests/integration/fixtures/TestActionConcurrencyGroupQueue/action_run_job.yml b/tests/integration/fixtures/TestActionConcurrencyGroupQueue/action_run_job.yml new file mode 100644 index 0000000000..c8c12085d3 --- /dev/null +++ b/tests/integration/fixtures/TestActionConcurrencyGroupQueue/action_run_job.yml @@ -0,0 +1,42 @@ +- + id: 500 + run_id: 500 + repo_id: 4 + owner_id: 1 + commit_sha: 985f0301dba5e7b34be866819cd15ad3d8f508ee + is_fork_pull_request: 0 + name: job_1 + attempt: 0 + job_id: job_1 + task_id: 0 + status: 5 # StatusWaiting + runs_on: '["fedora"]' + created: 1758848614 +- + id: 501 + run_id: 501 + repo_id: 4 + owner_id: 1 + commit_sha: 985f0301dba5e7b34be866819cd15ad3d8f508ee + is_fork_pull_request: 0 + name: job_1 + attempt: 0 + job_id: job_1 + task_id: 0 + status: 5 # StatusWaiting + runs_on: '["fedora"]' + created: 1758848614 +- + id: 502 + run_id: 502 + repo_id: 4 + owner_id: 1 + commit_sha: 985f0301dba5e7b34be866819cd15ad3d8f508ee + is_fork_pull_request: 0 + name: job_1 + attempt: 0 + job_id: job_1 + task_id: 0 + status: 5 # StatusWaiting + runs_on: '["fedora"]' + created: 1758848614 diff --git a/tests/integration/fixtures/TestActionConcurrencyGroupQueue/action_runner.yml b/tests/integration/fixtures/TestActionConcurrencyGroupQueue/action_runner.yml new file mode 100644 index 0000000000..d783f83110 --- /dev/null +++ b/tests/integration/fixtures/TestActionConcurrencyGroupQueue/action_runner.yml @@ -0,0 +1,7 @@ +- + id: 1004 + uuid: "fb857e63-c0ce-4571-a6c9-fde26c128073" + name: "Global runner" + owner_id: 0 + repo_id: 0 + deleted: 0 diff --git a/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/action_run.yml b/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/action_run.yml new file mode 100644 index 0000000000..07e88f868c --- /dev/null +++ b/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/action_run.yml @@ -0,0 +1,18 @@ +- + id: 500 + index: 1 + status: 5 # StatusWaiting + repo_id: 62 + owner_id: 2 +- + id: 501 + index: 2 + status: 5 # StatusWaiting + repo_id: 3 + owner_id: 3 +- + id: 502 + index: 2 + status: 5 # StatusWaiting + repo_id: 1 + owner_id: 2 diff --git a/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/action_run_job.yml b/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/action_run_job.yml new file mode 100644 index 0000000000..17d5b6426a --- /dev/null +++ b/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/action_run_job.yml @@ -0,0 +1,42 @@ +- + id: 500 + run_id: 500 + repo_id: 62 + owner_id: 2 + commit_sha: 985f0301dba5e7b34be866819cd15ad3d8f508ee + is_fork_pull_request: 0 + name: job_1 + attempt: 0 + job_id: job_1 + task_id: 0 + status: 5 # StatusWaiting + runs_on: '["fedora"]' + created: 1758848614 +- + id: 501 + run_id: 501 + repo_id: 4 + owner_id: 3 + commit_sha: 985f0301dba5e7b34be866819cd15ad3d8f508ee + is_fork_pull_request: 0 + name: job_1 + attempt: 0 + job_id: job_1 + task_id: 0 + status: 5 # StatusWaiting + runs_on: '["fedora"]' + created: 1758848614 +- + id: 502 + run_id: 502 + repo_id: 1 + owner_id: 2 + commit_sha: 985f0301dba5e7b34be866819cd15ad3d8f508ee + is_fork_pull_request: 0 + name: job_1 + attempt: 0 + job_id: job_1 + task_id: 0 + status: 5 # StatusWaiting + runs_on: '["fedora"]' + created: 1758848614 diff --git a/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/action_runner.yml b/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/action_runner.yml new file mode 100644 index 0000000000..95599b19bd --- /dev/null +++ b/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/action_runner.yml @@ -0,0 +1,31 @@ +- + id: 1001 + uuid: "43b5d4d3-401b-42f9-94df-a9d45b447b82" + name: "User runner" + owner_id: 2 + repo_id: 0 + deleted: 0 + +- + id: 1002 + uuid: "bdc77f4f-2b2b-442d-bd44-e808f4306347" + name: "Organisation runner" + owner_id: 3 + repo_id: 0 + deleted: 0 + +- + id: 1003 + uuid: "9268bc8c-efbf-4dbe-aeb5-945733cdd098" + name: "Repository runner" + owner_id: 0 + repo_id: 1 + deleted: 0 + +- + id: 1004 + uuid: "fb857e63-c0ce-4571-a6c9-fde26c128073" + name: "Global runner" + owner_id: 0 + repo_id: 0 + deleted: 0 diff --git a/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/repo_unit.yml b/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/repo_unit.yml new file mode 100644 index 0000000000..84c2b7ad86 --- /dev/null +++ b/tests/integration/fixtures/TestActionConcurrencyRunnerFiltering/repo_unit.yml @@ -0,0 +1,6 @@ +- + id: 200 + repo_id: 3 + type: 10 + config: "{}" + created_unix: 946684810 diff --git a/tests/integration/fixtures/TestPullEditable/issue.yml b/tests/integration/fixtures/TestPullEditable/issue.yml new file mode 100644 index 0000000000..449a0b8f2b --- /dev/null +++ b/tests/integration/fixtures/TestPullEditable/issue.yml @@ -0,0 +1,16 @@ +- + id: 23 + repo_id: 10 + index: 2 + poster_id: 13 + original_author_id: 0 + name: pr2 + content: a pull request + milestone_id: 0 + priority: 0 + is_closed: false + is_pull: true + num_comments: 0 + created_unix: 946684820 + updated_unix: 978307180 + is_locked: false diff --git a/tests/integration/fixtures/TestPullEditable/pull_request.yml b/tests/integration/fixtures/TestPullEditable/pull_request.yml new file mode 100644 index 0000000000..f1b91a429c --- /dev/null +++ b/tests/integration/fixtures/TestPullEditable/pull_request.yml @@ -0,0 +1,12 @@ +- + id: 11 + type: 0 # gitea pull request + status: 2 # mergeable + issue_id: 23 + index: 2 + head_repo_id: 11 + base_repo_id: 10 + head_branch: branch2 + base_branch: master + merge_base: 0abcb056019adb83 + has_merged: false diff --git a/tests/integration/forgejo_confirmation_repo_test.go b/tests/integration/forgejo_confirmation_repo_test.go index 28d6777aea..985e99b2c7 100644 --- a/tests/integration/forgejo_confirmation_repo_test.go +++ b/tests/integration/forgejo_confirmation_repo_test.go @@ -9,7 +9,7 @@ import ( "testing" "forgejo.org/modules/translation" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -53,7 +53,7 @@ func TestDangerZoneConfirmation(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "success%3DThis%2Brepository%2Bhas%2Bbeen%2Bmarked%2Bfor%2Btransfer%2Band%2Bawaits%2Bconfirmation%2Bfrom%2B%2522User%2BOne%2522", flashCookie.Value) }) @@ -83,7 +83,7 @@ func TestDangerZoneConfirmation(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "success%3DThe%2Bfork%2Bhas%2Bbeen%2Bconverted%2Binto%2Ba%2Bregular%2Brepository.", flashCookie.Value) }) @@ -116,7 +116,7 @@ func TestDangerZoneConfirmation(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "success%3DThe%2Brepository%2Bwiki%2527s%2Bbranch%2Bname%2Bhas%2Bbeen%2Bsuccessfully%2Bnormalized.", flashCookie.Value) }) @@ -146,7 +146,7 @@ func TestDangerZoneConfirmation(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "success%3DThe%2Brepository%2Bwiki%2Bdata%2Bhas%2Bbeen%2Bdeleted.", flashCookie.Value) }) @@ -176,7 +176,7 @@ func TestDangerZoneConfirmation(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "success%3DThe%2Brepository%2Bhas%2Bbeen%2Bdeleted.", flashCookie.Value) }) diff --git a/tests/integration/forgejo_git_test.go b/tests/integration/forgejo_git_test.go index 693d6d9209..28ee572b8d 100644 --- a/tests/integration/forgejo_git_test.go +++ b/tests/integration/forgejo_git_test.go @@ -25,7 +25,7 @@ import ( ) func TestActionsUserGit(t *testing.T) { - onGiteaRun(t, testActionsUserGit) + onApplicationRun(t, testActionsUserGit) } func NewActionsUserTestContext(t *testing.T, username, reponame string) APITestContext { diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index 5be5c6088c..1d8f44c8dc 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -62,7 +62,7 @@ func createSSHUrl(gitPath string, u *url.URL) *url.URL { var rootPathRe = regexp.MustCompile("\\[repository\\]\nROOT\\s=\\s.*") -func onGiteaRun[T testing.TB](t T, callback func(T, *url.URL)) { +func onApplicationRun[T testing.TB](t T, callback func(T, *url.URL)) { defer tests.PrepareTestEnv(t, 1)() s := http.Server{ Handler: testWebRoutes, diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go index 977385ff6a..61e2a65f97 100644 --- a/tests/integration/git_push_test.go +++ b/tests/integration/git_push_test.go @@ -38,7 +38,7 @@ func forEachObjectFormat(t *testing.T, f func(t *testing.T, objectFormat git.Obj } func TestGitPush(t *testing.T) { - onGiteaRun(t, testGitPush) + onApplicationRun(t, testGitPush) } func testGitPush(t *testing.T, u *url.URL) { @@ -205,7 +205,7 @@ func runTestGitPush(t *testing.T, u *url.URL, objectFormat git.ObjectFormat, git } func TestOptionsGitPush(t *testing.T) { - onGiteaRun(t, testOptionsGitPush) + onApplicationRun(t, testOptionsGitPush) } func testOptionsGitPush(t *testing.T, u *url.URL) { diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go index 84de08751d..3608f9fd5b 100644 --- a/tests/integration/git_smart_http_test.go +++ b/tests/integration/git_smart_http_test.go @@ -24,7 +24,7 @@ import ( ) func TestGitSmartHTTP(t *testing.T) { - onGiteaRun(t, testGitSmartHTTP) + onApplicationRun(t, testGitSmartHTTP) } func testGitSmartHTTP(t *testing.T, u *url.URL) { diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index fcd81cf6b2..f596f7b05f 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -34,7 +34,7 @@ import ( "forgejo.org/modules/lfs" "forgejo.org/modules/setting" api "forgejo.org/modules/structs" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" files_service "forgejo.org/services/repository/files" "forgejo.org/tests" @@ -49,11 +49,11 @@ const ( ) func TestGit(t *testing.T) { - onGiteaRun(t, testGit) + onApplicationRun(t, testGit) } func TestActionsTokenAuth(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47}) task.GenerateToken() actions_model.UpdateTask(db.DefaultContext, task) @@ -495,7 +495,7 @@ func doProtectBranch(ctx APITestContext, branch string, addParameter ...paramete req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), parameter) ctx.Session.MakeRequest(t, req, http.StatusSeeOther) // Check if master branch has been locked successfully - flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := ctx.Session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522"+url.QueryEscape(branch)+"%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) } @@ -1098,7 +1098,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string } func TestDataAsync_Issue29101(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index ac3c86e332..3ee6882e3b 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -41,7 +41,7 @@ import ( "forgejo.org/modules/web" "forgejo.org/routers" "forgejo.org/services/auth/source/remote" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" "forgejo.org/services/mailer" user_service "forgejo.org/services/user" "forgejo.org/tests" @@ -297,7 +297,7 @@ func (s *TestSession) EnrollTOTP(t testing.TB) { }) s.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := s.GetCookie(gitea_context.CookieNameFlash) + flashCookie := s.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success%3DYour%2Baccount%2Bhas%2Bbeen%2Bsuccessfully%2Benrolled.") } @@ -522,7 +522,7 @@ func createApplicationSettingsToken(t testing.TB, session *TestSession, name str // Log the flash values on failure if !assert.Equal(t, []string{"/user/settings/applications"}, resp.Result().Header["Location"]) { for _, cookie := range resp.Result().Cookies() { - if cookie.Name != gitea_context.CookieNameFlash { + if cookie.Name != app_context.CookieNameFlash { continue } flash, _ := url.ParseQuery(cookie.Value) @@ -668,7 +668,7 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { if len(respBytes) == 0 { // log the content of the flash cookie for _, cookie := range recorder.Result().Cookies() { - if cookie.Name != gitea_context.CookieNameFlash { + if cookie.Name != app_context.CookieNameFlash { continue } flash, _ := url.ParseQuery(cookie.Value) diff --git a/tests/integration/issue_subscribe_test.go b/tests/integration/issue_subscribe_test.go index 533526c388..bdd8c4eb27 100644 --- a/tests/integration/issue_subscribe_test.go +++ b/tests/integration/issue_subscribe_test.go @@ -13,7 +13,7 @@ import ( ) func TestIssueSubscribe(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := emptyTestSession(t) testIssueSubscribe(t, *session, true) }) diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index e203adf245..8f0add1819 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -86,6 +86,21 @@ func TestViewIssues(t *testing.T) { assert.Equal(t, "Search issues…", placeholder) } +func TestViewIssuesType(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + session := loginUser(t, user.Name) + req := NewRequest(t, "GET", repo.Link()+"/issues") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + issuesType := htmlDoc.doc.Find(".list-header-type > .menu .item[href*=\"type=all\"]").First() + assert.Equal(t, "All issues", issuesType.Text()) +} + func TestViewIssuesSortByType(t *testing.T) { defer tests.PrepareTestEnv(t)() @@ -1305,7 +1320,7 @@ func TestIssueFilterNoFollow(t *testing.T) { } func TestIssueForm(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) repo, _, f := tests.CreateDeclarativeRepo(t, user2, "", @@ -1354,7 +1369,7 @@ body: } func TestIssueUnsubscription(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{ AutoInit: optional.Some(false), diff --git a/tests/integration/linguist_test.go b/tests/integration/linguist_test.go index 85080c1d2e..efeaaabb98 100644 --- a/tests/integration/linguist_test.go +++ b/tests/integration/linguist_test.go @@ -24,7 +24,7 @@ import ( ) func TestLinguistSupport(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { /****************** ** Preparations ** ******************/ diff --git a/tests/integration/migrate_test.go b/tests/integration/migrate_test.go index dc11101d97..75c4a4cfb3 100644 --- a/tests/integration/migrate_test.go +++ b/tests/integration/migrate_test.go @@ -59,7 +59,7 @@ func TestMigrateLocalPath(t *testing.T) { } func TestMigrate(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.AppVer, "1.16.0")() require.NoError(t, migrations.Init()) @@ -115,7 +115,7 @@ func TestMigrate(t *testing.T) { } func TestMigrateWithWiki(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.AppVer, "1.16.0")() require.NoError(t, migrations.Init()) @@ -174,7 +174,7 @@ func TestMigrateWithWiki(t *testing.T) { } func TestMigrateWithReleases(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.AppVer, "1.16.0")() require.NoError(t, migrations.Init()) diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go index d62afbf0f7..04ed353edc 100644 --- a/tests/integration/migration-test/migration_test.go +++ b/tests/integration/migration-test/migration_test.go @@ -17,8 +17,8 @@ import ( "testing" "forgejo.org/models/db" - "forgejo.org/models/migrations" - migrate_base "forgejo.org/models/migrations/base" + "forgejo.org/models/gitea_migrations" + migrate_base "forgejo.org/models/gitea_migrations/base" "forgejo.org/models/unittest" "forgejo.org/modules/base" "forgejo.org/modules/charset" @@ -262,7 +262,7 @@ func restoreOldDB(t *testing.T, version string) bool { func wrappedMigrate(x *xorm.Engine) error { currentEngine = x - return migrations.Migrate(x) + return gitea_migrations.Migrate(x) } func doMigrationTest(t *testing.T, version string) { diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go index 23bb550d9f..f9d931a76e 100644 --- a/tests/integration/mirror_push_test.go +++ b/tests/integration/mirror_push_test.go @@ -31,7 +31,7 @@ import ( api "forgejo.org/modules/structs" "forgejo.org/modules/test" "forgejo.org/modules/translation" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" doctor "forgejo.org/services/doctor" "forgejo.org/services/migrations" mirror_service "forgejo.org/services/mirror" @@ -84,7 +84,7 @@ func TestPushMirrorRedactCredential(t *testing.T) { } func TestMirrorPush(t *testing.T) { - onGiteaRun(t, testMirrorPush) + onApplicationRun(t, testMirrorPush) } func testMirrorPush(t *testing.T, u *url.URL) { @@ -173,7 +173,7 @@ func doCreatePushMirror(ctx APITestContext, address, username, password string) }) ctx.Session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := ctx.Session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success") } @@ -194,7 +194,7 @@ func doCreatePushMirrorWithBranchFilter(ctx APITestContext, address, username, p }) ctx.Session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := ctx.Session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success") } @@ -215,7 +215,7 @@ func doRemovePushMirror(ctx APITestContext, address, username, password string, }) ctx.Session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := ctx.Session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success") } @@ -227,7 +227,7 @@ func TestSSHPushMirror(t *testing.T) { t.Skip("SSH executable not present") } - onGiteaRun(t, func(t *testing.T, _ *url.URL) { + onApplicationRun(t, func(t *testing.T, _ *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.Mirror.Enabled, true)() defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())() @@ -308,7 +308,7 @@ func TestSSHPushMirror(t *testing.T) { }) sess.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := sess.GetCookie(gitea_context.CookieNameFlash) + flashCookie := sess.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success") @@ -389,7 +389,7 @@ func TestSSHPushMirror(t *testing.T) { } func TestPushMirrorBranchFilterWebUI(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.Mirror.Enabled, true)() require.NoError(t, migrations.Init()) @@ -490,7 +490,7 @@ func TestPushMirrorBranchFilterWebUI(t *testing.T) { } func TestPushMirrorBranchFilterIntegration(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.Mirror.Enabled, true)() require.NoError(t, migrations.Init()) @@ -579,7 +579,7 @@ func TestPushMirrorBranchFilterIntegration(t *testing.T) { } func TestPushMirrorSettings(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.Mirror.Enabled, true)() require.NoError(t, migrations.Init()) @@ -615,7 +615,7 @@ func TestPushMirrorSettings(t *testing.T) { }) sess.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := sess.GetCookie(gitea_context.CookieNameFlash) + flashCookie := sess.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success") }) @@ -650,7 +650,7 @@ func TestPushMirrorSettings(t *testing.T) { }) sess.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := sess.GetCookie(gitea_context.CookieNameFlash) + flashCookie := sess.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success") }) @@ -658,7 +658,7 @@ func TestPushMirrorSettings(t *testing.T) { } func TestPushMirrorBranchFilterSyncOperations(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.Mirror.Enabled, true)() require.NoError(t, migrations.Init()) @@ -892,7 +892,7 @@ func TestPushMirrorBranchFilterSyncOperations(t *testing.T) { } func TestPushMirrorWebUIToAPIIntegration(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() defer test.MockVariableValue(&setting.Mirror.Enabled, true)() require.NoError(t, migrations.Init()) diff --git a/tests/integration/new_org_test.go b/tests/integration/new_org_test.go index 5ea386573a..9159d86b69 100644 --- a/tests/integration/new_org_test.go +++ b/tests/integration/new_org_test.go @@ -15,7 +15,7 @@ import ( ) func TestNewOrganizationForm(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") locale := translation.NewLocale("en-US") diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index ebfd386ee0..e27201ac28 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -582,7 +582,7 @@ func TestSignInOAuthCallbackWithoutPKCEWhenUnsupported(t *testing.T) { } func TestSignInOAuthCallbackPKCE(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { // Setup authentication source sourceName := "oidc" authSource := addAuthSource(t, authSourcePayloadOpenIDConnect(sourceName, u.String())) diff --git a/tests/integration/org_count_test.go b/tests/integration/org_count_test.go index 93035c8e5b..35af738509 100644 --- a/tests/integration/org_count_test.go +++ b/tests/integration/org_count_test.go @@ -20,7 +20,7 @@ import ( ) func TestOrgCounts(t *testing.T) { - onGiteaRun(t, testOrgCounts) + onApplicationRun(t, testOrgCounts) } func testOrgCounts(t *testing.T, u *url.URL) { diff --git a/tests/integration/org_profile_test.go b/tests/integration/org_profile_test.go index 2cd80cc1de..381d11c95f 100644 --- a/tests/integration/org_profile_test.go +++ b/tests/integration/org_profile_test.go @@ -20,7 +20,7 @@ import ( ) func TestOrgProfile(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { checkReadme := func(t *testing.T, title, readmeFilename string, expectedCount int) { t.Run(title, func(t *testing.T) { defer tests.PrintCurrentTest(t)() diff --git a/tests/integration/patch_status_test.go b/tests/integration/patch_status_test.go index c54cad91c4..ef34f34903 100644 --- a/tests/integration/patch_status_test.go +++ b/tests/integration/patch_status_test.go @@ -32,7 +32,7 @@ import ( ) func TestPatchStatus(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) diff --git a/tests/integration/pathutils_test.go b/tests/integration/pathutils_test.go new file mode 100644 index 0000000000..6d08372d55 --- /dev/null +++ b/tests/integration/pathutils_test.go @@ -0,0 +1,210 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package integration + +import ( + "testing" + + "forgejo.org/services/repository/files" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSanitizePath(t *testing.T) { + tests := []struct { + name string + input string + expected string + expectError bool + }{ + // Valid paths + { + name: "simple valid path", + input: "folder/file.txt", + expected: "folder/file.txt", + }, + { + name: "single file", + input: "file.txt", + expected: "file.txt", + }, + { + name: "nested path", + input: "a/b/c/file.txt", + expected: "a/b/c/file.txt", + }, + + // Path normalization + { + name: "backslash to forward slash", + input: "folder\\file.txt", + expected: "folder/file.txt", + }, + { + name: "mixed separators", + input: "folder\\subfolder/file.txt", + expected: "folder/subfolder/file.txt", + }, + { + name: "double separators", + input: "folder//file.txt", + expected: "folder/file.txt", + }, + { + name: "trailing slash", + input: "folder/file.txt/", + expected: "folder/file.txt", + }, + { + name: "dot segments", + input: "folder/./file.txt", + expected: "folder/file.txt", + }, + { + name: "parent directory references", + input: "folder/../other/file.txt", + expected: "other/file.txt", + }, + { + name: "< and >", + input: "file.txt", + expected: "file.txt", + }, + { + name: ": and | and ? and *", + input: "file:name|with?bad*chars.txt", + expected: "file:name|with?bad*chars.txt", + }, + { + name: "control characters", + input: "file\x00\x01name.txt", + expected: "file\x00\x01name.txt", + }, + { + name: "only special characters", + input: "<>:\"|?*", + expected: "<>:\"|?*", + }, + + // Character sanitization + { + name: "quotes in filename", + input: `file"name.txt`, + expected: "file\"name.txt", + }, + + // Whitespace handling + { + name: "leading whitespace", + input: " file.txt", + expected: "file.txt", + }, + { + name: "trailing whitespace", + input: "file.txt ", + expected: "file.txt", + }, + { + name: "whitespace in path components", + input: " folder / file.txt ", + expected: "folder/file.txt", + }, + + // Edge cases that should return errors + { + name: "path starts with slash", + input: "/folder/file.txt", + expectError: true, + }, + { + name: "empty string", + input: "", + expectError: true, + }, + { + name: "only separators", + input: "///", + expectError: true, + }, + { + name: "only whitespace", + input: " ", + expectError: true, + }, + { + name: "path that resolves to root", + input: "../..", + expectError: true, + }, + { + name: "path that goes above root", + input: "folder/../../..", + expectError: true, + }, + + // Complex combinations + { + name: "complex path with multiple issues", + input: "folder\\with:special|chars/normal_file.txt", + expected: "folder/with:special|chars/normal_file.txt", + }, + { + name: "unicode characters preserved", + input: "folder/файл.txt", + expected: "folder/файл.txt", + }, + { + name: "dots and extensions", + input: "file.name.with.dots.txt", + expected: "file.name.with.dots.txt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := files.SanitizePath(tt.input) + + if tt.expectError { + require.Error(t, err, "expected error for input %q", tt.input) + return + } + + require.NoError(t, err, "unexpected error for input %q", tt.input) + assert.Equal(t, tt.expected, result, "SanitizePath(%q) should return expected result", tt.input) + }) + } +} + +// TestSanitizePathErrorMessages tests that error messages are informative +func TestSanitizePathErrorMessages(t *testing.T) { + tests := []struct { + name string + input string + expectedError string + }{ + { + name: "path starts with slash", + input: "/test/path", + expectedError: "path starts with / : /test/path", + }, + { + name: "path that resolves to root", + input: "../..", + expectedError: "path resolves to root or becomes empty after cleaning", + }, + { + name: "empty after sanitization", + input: "", + expectedError: "path resolves to root or becomes empty after cleaning", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := files.SanitizePath(tt.input) + require.Error(t, err, "expected error for input %q", tt.input) + assert.Equal(t, tt.expectedError, err.Error(), "error message for %q should match expected", tt.input) + }) + } +} diff --git a/tests/integration/proctected_branch_test.go b/tests/integration/proctected_branch_test.go index 5024b63c42..e7680e4017 100644 --- a/tests/integration/proctected_branch_test.go +++ b/tests/integration/proctected_branch_test.go @@ -19,7 +19,7 @@ import ( ) func TestProtectedBranch_AdminEnforcement(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "add-readme", "README.md", "WIP") diff --git a/tests/integration/pull_commit_test.go b/tests/integration/pull_commit_test.go index 1f9a6ffd22..562cbcb334 100644 --- a/tests/integration/pull_commit_test.go +++ b/tests/integration/pull_commit_test.go @@ -77,7 +77,7 @@ func TestPullCommitSignature(t *testing.T) { fromBranch := "master" toBranch := "branch-signed" - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { // Use a new GNUPGPHOME to avoid messing with the existing GPG keyring. tmpDir := t.TempDir() require.NoError(t, os.Chmod(tmpDir, 0o700)) diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 7af70b4e5c..16b05e0808 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -11,10 +11,12 @@ import ( "net/url" "path" "regexp" + "strconv" "strings" "testing" "forgejo.org/models/db" + issues_model "forgejo.org/models/issues" repo_model "forgejo.org/models/repo" unit_model "forgejo.org/models/unit" "forgejo.org/models/unittest" @@ -97,7 +99,7 @@ func testPullCreateDirectly(t *testing.T, session *TestSession, baseRepoOwner, b } func TestPullCreate(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") @@ -121,11 +123,17 @@ func TestPullCreate(t *testing.T) { assert.Regexp(t, "diff", resp.Body) assert.Regexp(t, `Subject: \[PATCH\] Update README.md`, resp.Body) assert.NotRegexp(t, "diff.*diff", resp.Body) // not two diffs, just one + + // Check that mergebase is set. + index, err := strconv.ParseInt(url[strings.LastIndexByte(url, '/')+1:], 10, 64) + require.NoError(t, err) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, HeadBranch: "master", BaseBranch: "master", Index: index}) + assert.NotEmpty(t, pr.MergeBase) }) } func TestPullCreateWithPullTemplate(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) forkUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -226,7 +234,7 @@ func TestPullCreateWithPullTemplate(t *testing.T) { } func TestPullCreate_TitleEscape(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") @@ -288,7 +296,7 @@ func testDeleteRepository(t *testing.T, session *TestSession, ownerName, repoNam } func TestPullBranchDelete(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther) @@ -314,7 +322,7 @@ func TestPullBranchDelete(t *testing.T) { } func TestRecentlyPushed(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") @@ -576,7 +584,7 @@ Test checks: Check if pull request can be created from base to the fork repository. */ func TestPullCreatePrFromBaseToFork(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { sessionFork := loginUser(t, "user1") testRepoFork(t, sessionFork, "user2", "repo1", "user1", "repo1") @@ -591,3 +599,43 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) { assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url) }) } + +func TestPullCreatePrFromForkToFork(t *testing.T) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { + sessionFork1 := loginUser(t, "user1") + testRepoFork(t, sessionFork1, "user2", "repo1", "user1", "repo1") + sessionFork3 := loginUser(t, "user30") + testRepoFork(t, sessionFork3, "user2", "repo1", "user30", "repo1") + + // Edit fork of user30 + testEditFileToNewBranch(t, sessionFork3, "user30", "repo1", "master", "my-patch", "README.md", "Hello, World (Edited)\n") + + // As user30, go to the PR page of the first fork, belonging to user1 + req := NewRequest(t, "GET", path.Join("user1", "repo1", "pulls")) + resp := sessionFork3.MakeRequest(t, req, http.StatusOK) + + // Check that the button to create PRs is enabled + htmlDoc := NewHTMLParser(t, resp.Body) + defaultCompareLink, exists := htmlDoc.doc.Find(".new-pr-button").Attr("href") + assert.True(t, exists, "The template has changed") + assert.Regexp(t, "^/user1/repo1/compare/.*$", defaultCompareLink) + + // The default compare page for the user1 fork should let us select a branch from our user30 fork + req = NewRequest(t, "GET", defaultCompareLink) + resp = sessionFork3.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + ourCompareLink, exists := htmlDoc.doc.Find(".head-branch-list .item:contains('user30:my-patch')").Attr("data-url") + assert.True(t, exists, "The branch from our fork is not proposed in the /compare page of their fork") + assert.Equal(t, "/user1/repo1/compare/master...user30/repo1:my-patch", ourCompareLink) + + // Go to the compare page for the branch we created, which should load fine + req = NewRequest(t, "GET", ourCompareLink) + sessionFork3.MakeRequest(t, req, http.StatusOK) + + // Create a PR + resp = testPullCreateDirectly(t, sessionFork3, "user1", "repo1", "master", "user30", "repo1", "my-patch", "This is a pull title") + // check the redirected URL + url := test.RedirectURL(resp) + assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url) + }) +} diff --git a/tests/integration/pull_diff_test.go b/tests/integration/pull_diff_test.go index 0f28bbbd49..aca826d4ab 100644 --- a/tests/integration/pull_diff_test.go +++ b/tests/integration/pull_diff_test.go @@ -69,7 +69,7 @@ func doTestPRDiff(t *testing.T, prDiffURL string, expectedFilenames []string, ed } func TestPullDiff_AGitNotEditable(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) diff --git a/tests/integration/pull_editable_test.go b/tests/integration/pull_editable_test.go index f2e6f2f52c..c4fa1620ac 100644 --- a/tests/integration/pull_editable_test.go +++ b/tests/integration/pull_editable_test.go @@ -5,40 +5,48 @@ package integration import ( "net/http" - "net/url" + "strings" "testing" auth_model "forgejo.org/models/auth" + "forgejo.org/models/unittest" api "forgejo.org/modules/structs" + "forgejo.org/modules/translation" "forgejo.org/tests" + + "github.com/stretchr/testify/assert" ) func TestPullEditable_ShowEditableLabel(t *testing.T) { - onGiteaRun(t, func(t *testing.T, forgejoURL *url.URL) { - t.Run("Show editable label if PR is editable", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - editable := true + // This fixture loads a PR which is made from a different repository, + // and opened by the user who owns the fork (which is necessary for + // them to be allowed to set the PR as editable). + defer unittest.OverrideFixtures("tests/integration/fixtures/TestPullEditable")() + defer tests.PrepareTestEnv(t)() - setPREditable(t, editable) - testEditableLabelShown(t, editable) - }) + t.Run("Show editable label if PR is editable", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + editable := true - t.Run("Don't show editable label if PR is not editable", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - editable := false + setPREditable(t, editable) + testEditableLabelShown(t, editable) + }) - setPREditable(t, editable) - testEditableLabelShown(t, editable) - }) + t.Run("Don't show editable label if PR is not editable", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + editable := false + + setPREditable(t, editable) + testEditableLabelShown(t, editable) }) } func setPREditable(t *testing.T, editable bool) { t.Helper() - session := loginUser(t, "user1") + session := loginUser(t, "user13") token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/pulls/3", &api.EditPullRequestOption{ + req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user12/repo10/pulls/2", &api.EditPullRequestOption{ AllowMaintainerEdit: &editable, }).AddTokenAuth(token) session.MakeRequest(t, req, http.StatusCreated) @@ -46,9 +54,17 @@ func setPREditable(t *testing.T, editable bool) { func testEditableLabelShown(t *testing.T, expectLabel bool) { t.Helper() - session := loginUser(t, "user2") - req := NewRequest(t, "GET", "/user2/repo1/pulls/3") + session := loginUser(t, "user12") + req := NewRequest(t, "GET", "/user12/repo10/pulls/2") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc.AssertElement(t, "#editable-label", expectLabel) + locale := translation.NewLocale("en-US") + if expectLabel { + sidebarText := htmlDoc.Find(".issue-content-right span.maintainers-can-edit-status").First().Text() + assert.Equal(t, locale.TrString("repo.pulls.maintainers_can_edit"), strings.TrimSpace(sidebarText)) + } else { + sidebarText := htmlDoc.Find(".issue-content-right span.maintainers-can-edit-status").First().Text() + assert.Equal(t, locale.TrString("repo.pulls.maintainers_cannot_edit"), strings.TrimSpace(sidebarText)) + } } diff --git a/tests/integration/pull_icon_test.go b/tests/integration/pull_icon_test.go index 9ab8f244cf..fbd87a7234 100644 --- a/tests/integration/pull_icon_test.go +++ b/tests/integration/pull_icon_test.go @@ -30,7 +30,7 @@ import ( ) func TestPullRequestIcons(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) repo, _, f := tests.CreateDeclarativeRepo(t, user, "pr-icons", []unit_model.Type{unit_model.TypeCode, unit_model.TypePullRequests}, nil, nil) defer f() diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index b8923dd6f4..e7ef680f3c 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -133,7 +133,7 @@ func retrieveHookTasks(t *testing.T, hookID int64, activateWebhook bool) []*webh } func TestPullMerge(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { hookTasks := retrieveHookTasks(t, 1, true) hookTasksLenBefore := len(hookTasks) @@ -153,7 +153,7 @@ func TestPullMerge(t *testing.T) { } func TestPullRebase(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { hookTasks := retrieveHookTasks(t, 1, true) hookTasksLenBefore := len(hookTasks) @@ -173,7 +173,7 @@ func TestPullRebase(t *testing.T) { } func TestPullRebaseMerge(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { hookTasks := retrieveHookTasks(t, 1, true) hookTasksLenBefore := len(hookTasks) @@ -193,7 +193,7 @@ func TestPullRebaseMerge(t *testing.T) { } func TestPullSquash(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { hookTasks := retrieveHookTasks(t, 1, true) hookTasksLenBefore := len(hookTasks) @@ -214,7 +214,7 @@ func TestPullSquash(t *testing.T) { } func TestPullCleanUpAfterMerge(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n") @@ -249,7 +249,7 @@ func TestPullCleanUpAfterMerge(t *testing.T) { } func TestCantMergeWorkInProgress(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") @@ -268,7 +268,7 @@ func TestCantMergeWorkInProgress(t *testing.T) { } func TestCantMergeConflict(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") @@ -338,7 +338,7 @@ func TestCantMergeConflict(t *testing.T) { } func TestCantMergeUnrelated(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n") @@ -433,7 +433,7 @@ func TestCantMergeUnrelated(t *testing.T) { } func TestFastForwardOnlyMerge(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n") @@ -474,7 +474,7 @@ func TestFastForwardOnlyMerge(t *testing.T) { } func TestCantFastForwardOnlyMergeDiverging(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n") @@ -517,7 +517,7 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) { } func TestPullRetargetChildOnBranchDelete(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testEditFileToNewBranch(t, session, "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") @@ -547,7 +547,7 @@ func TestPullRetargetChildOnBranchDelete(t *testing.T) { } func TestPullDontRetargetChildOnWrongRepo(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n") @@ -577,7 +577,7 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) { } func TestPullMergeIndexerNotifier(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { // create a pull request session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") @@ -650,7 +650,7 @@ func testResetRepo(t *testing.T, repoPath, branch, commitID string) { } func TestPullMergeBranchProtect(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { admin := "user1" owner := "user5" notOwner := "user4" @@ -925,7 +925,7 @@ func testPullAutoMergeAfterCommitStatusSucceed(t *testing.T, ctx APITestContext, } func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { for _, testCase := range []struct { name string forkName string @@ -981,7 +981,7 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) { } func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { // create a pull request baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) @@ -1106,7 +1106,7 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing. } func TestPullDeleteBranchPerms(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { user2Session := loginUser(t, "user2") user4Session := loginUser(t, "user4") testRepoFork(t, user4Session, "user2", "repo1", "user4", "repo1") @@ -1138,7 +1138,7 @@ func TestPullDeleteBranchPerms(t *testing.T) { // Test that rebasing only happens when its necessary. func TestRebaseWhenNecessary(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") diff --git a/tests/integration/pull_reopen_test.go b/tests/integration/pull_reopen_test.go index cf95f6b730..937bc4c2ac 100644 --- a/tests/integration/pull_reopen_test.go +++ b/tests/integration/pull_reopen_test.go @@ -20,7 +20,7 @@ import ( user_model "forgejo.org/models/user" "forgejo.org/modules/git" "forgejo.org/modules/translation" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" issue_service "forgejo.org/services/issue" pull_service "forgejo.org/services/pull" repo_service "forgejo.org/services/repository" @@ -32,7 +32,7 @@ import ( ) func TestPullrequestReopen(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26}) @@ -152,7 +152,7 @@ func TestPullrequestReopen(t *testing.T) { }) session.MakeRequest(t, req, http.StatusOK) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522"+branchName+"%2522%2Bhas%2Bbeen%2Brestored.") } @@ -166,7 +166,7 @@ func TestPullrequestReopen(t *testing.T) { }) session.MakeRequest(t, req, http.StatusOK) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522"+branchName+"%2522%2Bhas%2Bbeen%2Bdeleted.") } diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go index 131e8537c7..ea1c9a1be2 100644 --- a/tests/integration/pull_review_test.go +++ b/tests/integration/pull_review_test.go @@ -72,7 +72,7 @@ func loadComment(t *testing.T, commentID string) *issues_model.Comment { } func TestPullView_SelfReviewNotification(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { user1Session := loginUser(t, "user1") user2Session := loginUser(t, "user2") @@ -349,7 +349,7 @@ func TestPullView_ResolveInvalidatedReviewComment(t *testing.T) { } func TestPullView_CodeOwner(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo, _, f := tests.CreateDeclarativeRepo(t, user2, "test_codeowner", nil, nil, []*files_service.ChangeRepoFile{ @@ -448,7 +448,7 @@ func TestPullView_CodeOwner(t *testing.T) { } func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { user1Session := loginUser(t, "user1") user2Session := loginUser(t, "user2") @@ -582,7 +582,7 @@ func TestPullReview_OldLatestCommitId(t *testing.T) { } func TestPullReviewInArchivedRepo(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user2") // Open a PR @@ -821,7 +821,7 @@ func updateFileInBranch(user *user_model.User, repo *repo_model.Repository, tree } func TestPullRequestStaleReview(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index 9786c559ea..f0b90ff4d6 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -21,7 +21,7 @@ import ( ) func TestPullCreate_CommitStatus(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") @@ -120,7 +120,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) { // Reason: gitflow and merging master back into develop, where is high possibility, there are no changes // but just commit saying "Merge branch". And this meta commit can be also tagged, // so we need to have this meta commit also in develop branch. - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") @@ -145,7 +145,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) { } func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther) diff --git a/tests/integration/pull_summary_test.go b/tests/integration/pull_summary_test.go index 75c12720bf..d5e751bf80 100644 --- a/tests/integration/pull_summary_test.go +++ b/tests/integration/pull_summary_test.go @@ -14,7 +14,7 @@ import ( ) func TestPullSummaryCommits(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { testUser := "user2" testRepo := "repo1" branchOld := "master" diff --git a/tests/integration/pull_test.go b/tests/integration/pull_test.go index 8ff715a1b5..b4a575d808 100644 --- a/tests/integration/pull_test.go +++ b/tests/integration/pull_test.go @@ -30,6 +30,21 @@ func TestViewPulls(t *testing.T) { assert.Equal(t, "Search pulls…", placeholder) } +func TestViewPullsType(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + session := loginUser(t, user.Name) + req := NewRequest(t, "GET", repo.Link()+"/pulls") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + pullsType := htmlDoc.doc.Find(".list-header-type > .menu .item[href*=\"type=all\"]").First() + assert.Equal(t, "All pull requests", pullsType.Text()) +} + func TestPullViewConversation(t *testing.T) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go index 692699d24f..ad117355e5 100644 --- a/tests/integration/pull_update_test.go +++ b/tests/integration/pull_update_test.go @@ -30,7 +30,7 @@ import ( ) func TestAPIPullUpdate(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { // Create PR to test user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26}) @@ -59,7 +59,7 @@ func TestAPIPullUpdate(t *testing.T) { } func TestAPIPullUpdateByRebase(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { // Create PR to test user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26}) @@ -88,7 +88,7 @@ func TestAPIPullUpdateByRebase(t *testing.T) { } func TestAPIViewUpdateSettings(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { // Create PR to test user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26}) @@ -122,13 +122,13 @@ func TestAPIViewUpdateSettings(t *testing.T) { } func TestViewPullUpdateByMerge(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { testViewPullUpdate(t, "merge") }) } func TestViewPullUpdateByRebase(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { testViewPullUpdate(t, "rebase") }) } @@ -273,7 +273,7 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod } func TestStatusDuringUpdate(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") // Adjust this pull request to be in the conflict checker and having a head diff --git a/tests/integration/pull_wip_convert_test.go b/tests/integration/pull_wip_convert_test.go index 935636bd7f..ef63d8751c 100644 --- a/tests/integration/pull_wip_convert_test.go +++ b/tests/integration/pull_wip_convert_test.go @@ -14,7 +14,7 @@ import ( ) func TestPullWIPConvertSidebar(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { testRepo := "repo1" branchOld := "master" branchNew := "wip" diff --git a/tests/integration/quota_use_test.go b/tests/integration/quota_use_test.go index 105c2305d0..29a4972651 100644 --- a/tests/integration/quota_use_test.go +++ b/tests/integration/quota_use_test.go @@ -35,7 +35,7 @@ import ( ) func TestWebQuotaEnforcementRepoMigrate(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -48,7 +48,7 @@ func TestWebQuotaEnforcementRepoMigrate(t *testing.T) { } func TestWebQuotaEnforcementRepoCreate(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -57,7 +57,7 @@ func TestWebQuotaEnforcementRepoCreate(t *testing.T) { } func TestWebQuotaEnforcementRepoFork(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -69,7 +69,7 @@ func TestWebQuotaEnforcementRepoFork(t *testing.T) { } func TestWebQuotaEnforcementIssueAttachment(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -94,7 +94,7 @@ func TestWebQuotaEnforcementIssueAttachment(t *testing.T) { } func TestWebQuotaEnforcementMirrorSync(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -115,7 +115,7 @@ func TestWebQuotaEnforcementMirrorSync(t *testing.T) { } func TestWebQuotaEnforcementRepoContentEditing(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -164,7 +164,7 @@ func TestWebQuotaEnforcementRepoContentEditing(t *testing.T) { } func TestWebQuotaEnforcementRepoBranches(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -227,7 +227,7 @@ func TestWebQuotaEnforcementRepoBranches(t *testing.T) { } func TestWebQuotaEnforcementRepoReleases(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -262,7 +262,7 @@ func TestWebQuotaEnforcementRepoReleases(t *testing.T) { } func TestWebQuotaEnforcementRepoPulls(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -300,7 +300,7 @@ func TestWebQuotaEnforcementRepoPulls(t *testing.T) { } func TestWebQuotaEnforcementRepoTransfer(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -366,7 +366,7 @@ func TestWebQuotaEnforcementRepoTransfer(t *testing.T) { } func TestGitQuotaEnforcement(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() @@ -549,7 +549,7 @@ func TestGitQuotaEnforcement(t *testing.T) { } func TestQuotaConfigDefault(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { env := createQuotaWebEnv(t) defer env.Cleanup() diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go index 8e2ec20c76..ca975fb132 100644 --- a/tests/integration/release_test.go +++ b/tests/integration/release_test.go @@ -357,14 +357,46 @@ func TestDownloadReleaseAttachment(t *testing.T) { url := repo.Link() + "/releases/download/v1.1/README.md" + // user2/repo2 is private and can't be accessed anonymously req := NewRequest(t, "GET", url) MakeRequest(t, req, http.StatusNotFound) + // But the owner can access it req = NewRequest(t, "GET", url) session := loginUser(t, "user2") session.MakeRequest(t, req, http.StatusOK) } +func TestReleaseAttachmentDownloadCounter(t *testing.T) { + defer tests.PrepareTestEnv(t)() + tests.PrepareAttachmentsStorage(t) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + session := loginUser(t, "user2") + zipAttachmentLink := fmt.Sprintf("%s/archive/v1.1.zip", repo.Link()) + gzAttachmentLink := fmt.Sprintf("%s/archive/v1.1.tar.gz", repo.Link()) + counterSelector := "details.download > ul > li:has(a[href='%s']) span" + + // Assert zero downloads initially + doc := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s/releases", repo.Link())), http.StatusOK).Body) + zipDownloads := doc.Find(fmt.Sprintf(counterSelector, zipAttachmentLink)).Text() + gzDownloads := doc.Find(fmt.Sprintf(counterSelector, gzAttachmentLink)).Text() + assert.Contains(t, zipDownloads, "0 downloads") + assert.Contains(t, gzDownloads, "0 downloads") + + // Generate downloads + session.MakeRequest(t, NewRequest(t, "GET", zipAttachmentLink), http.StatusOK) + session.MakeRequest(t, NewRequest(t, "GET", gzAttachmentLink), http.StatusOK) + session.MakeRequest(t, NewRequest(t, "GET", gzAttachmentLink), http.StatusOK) + + // Check the new numbers + doc = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s/releases", repo.Link())), http.StatusOK).Body) + zipDownloads = doc.Find(fmt.Sprintf(counterSelector, zipAttachmentLink)).Text() + gzDownloads = doc.Find(fmt.Sprintf(counterSelector, gzAttachmentLink)).Text() + assert.Contains(t, zipDownloads, "1 download") + assert.Contains(t, gzDownloads, "2 downloads") +} + func TestReleaseHideArchiveLinksUI(t *testing.T) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/rename_branch_test.go b/tests/integration/rename_branch_test.go index 2029a3732b..6b186e38f2 100644 --- a/tests/integration/rename_branch_test.go +++ b/tests/integration/rename_branch_test.go @@ -11,14 +11,14 @@ import ( git_model "forgejo.org/models/git" repo_model "forgejo.org/models/repo" "forgejo.org/models/unittest" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" "forgejo.org/tests" "github.com/stretchr/testify/assert" ) func TestRenameBranch(t *testing.T) { - onGiteaRun(t, testRenameBranch) + onApplicationRun(t, testRenameBranch) } func testRenameBranch(t *testing.T, u *url.URL) { @@ -105,7 +105,7 @@ func testRenameBranch(t *testing.T, u *url.URL) { "to": "branch1", }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "error") @@ -133,7 +133,7 @@ func testRenameBranch(t *testing.T, u *url.URL) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie = session.GetCookie(gitea_context.CookieNameFlash) + flashCookie = session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success") @@ -163,7 +163,7 @@ func testRenameBranch(t *testing.T, u *url.URL) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "error%3DCannot%2Brename%2Bbranch%2Bmain2%2Bbecause%2Bit%2Bis%2Ba%2Bprotected%2Bbranch.", flashCookie.Value) diff --git a/tests/integration/repo_activity_test.go b/tests/integration/repo_activity_test.go index 967a55cad6..e30f92daa3 100644 --- a/tests/integration/repo_activity_test.go +++ b/tests/integration/repo_activity_test.go @@ -1,4 +1,5 @@ // Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -27,7 +28,7 @@ import ( ) func TestRepoActivity(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { session := loginUser(t, "user1") // Create PRs (1 merged & 2 proposed) @@ -75,6 +76,9 @@ func TestRepoActivity(t *testing.T) { assert.Equal(t, []string{"Pre-release", "Release", "Tag"}, labels) assert.Equal(t, []string{"", "v0.1 Pre-release", "v1 Release"}, titles) + // Active pull requests + assert.Contains(t, htmlDoc.Find(".grid .column:first-child").Text(), "3 active pull requests") + // Should be 1 merged pull request list = htmlDoc.doc.Find("#merged-pull-requests").Next().Find("p.desc") assert.Len(t, list.Nodes, 1) @@ -85,6 +89,9 @@ func TestRepoActivity(t *testing.T) { assert.Len(t, list.Nodes, 2) assert.Equal(t, "Proposed", list.Find(".label").First().Text()) + // Active issues + assert.Contains(t, htmlDoc.Find(".grid .column:last-child").Text(), "3 active issues") + // Should be 0 closed issues list = htmlDoc.doc.Find("#closed-issues").Next().Find("p.desc") assert.Empty(t, list.Nodes) diff --git a/tests/integration/repo_archive_test.go b/tests/integration/repo_archive_test.go index f0ffedfd9b..e5f5c5be0f 100644 --- a/tests/integration/repo_archive_test.go +++ b/tests/integration/repo_archive_test.go @@ -46,7 +46,7 @@ func TestRepoDownloadArchive(t *testing.T) { } func TestRepoDownloadArchiveSubdir(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { defer test.MockVariableValue(&setting.EnableGzip, true)() defer test.MockVariableValue(&web.GzipMinSize, 10)() defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() diff --git a/tests/integration/repo_archive_text_test.go b/tests/integration/repo_archive_text_test.go index db133ce7d7..e8343f3fef 100644 --- a/tests/integration/repo_archive_text_test.go +++ b/tests/integration/repo_archive_text_test.go @@ -20,7 +20,7 @@ import ( ) func TestArchiveText(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { testUser := "user2" user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: testUser}) session := loginUser(t, testUser) diff --git a/tests/integration/repo_badges_test.go b/tests/integration/repo_badges_test.go index 928a9975fe..e4a62d8cfc 100644 --- a/tests/integration/repo_badges_test.go +++ b/tests/integration/repo_badges_test.go @@ -31,7 +31,7 @@ import ( ) func TestBadges(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { prep := func(t *testing.T) (*repo_model.Repository, func()) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go index 3cb6e42c89..1b12180eca 100644 --- a/tests/integration/repo_branch_test.go +++ b/tests/integration/repo_branch_test.go @@ -46,7 +46,7 @@ func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubU } func TestCreateBranch(t *testing.T) { - onGiteaRun(t, testCreateBranches) + onApplicationRun(t, testCreateBranches) } func testCreateBranches(t *testing.T, giteaURL *url.URL) { @@ -161,7 +161,7 @@ func TestCreateBranchInvalidCSRF(t *testing.T) { } func TestDatabaseMissingABranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, URL *url.URL) { + onApplicationRun(t, func(t *testing.T, URL *url.URL) { session := loginUser(t, "user2") // Create two branches @@ -206,7 +206,7 @@ func TestDatabaseMissingABranch(t *testing.T) { } func TestCreateBranchButtonVisibility(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") t.Run("Check create branch button", func(t *testing.T) { diff --git a/tests/integration/repo_citation_test.go b/tests/integration/repo_citation_test.go index 5651ac3db3..7a3251d987 100644 --- a/tests/integration/repo_citation_test.go +++ b/tests/integration/repo_citation_test.go @@ -20,7 +20,7 @@ import ( ) func TestCitation(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) session := loginUser(t, user.LoginName) diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index c986164b50..cb8f398257 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -75,7 +75,7 @@ func testRepoForkLegacyRedirect(t *testing.T, session *TestSession, ownerName, r } func TestRepoFork(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user5"}) session := loginUser(t, user5.Name) @@ -210,7 +210,7 @@ func TestRepoFork(t *testing.T) { } func TestRepoForkToOrg(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"}) @@ -243,7 +243,7 @@ func TestRepoForkToOrg(t *testing.T) { func TestForkListPrivateRepo(t *testing.T) { forkItemSelector := ".tw-flex.tw-items-center.tw-py-2" - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user5") org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23, Visibility: structs.VisibleTypePrivate}) diff --git a/tests/integration/repo_generate_test.go b/tests/integration/repo_generate_test.go index 44987c14b0..6a61e36668 100644 --- a/tests/integration/repo_generate_test.go +++ b/tests/integration/repo_generate_test.go @@ -224,7 +224,7 @@ func TestRepoCreateFormTrimSpace(t *testing.T) { } func TestRepoGenerateTemplating(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { input := `# $REPO_NAME This is a Repo By $REPO_OWNER ThisIsThe${REPO_NAME}InAnInlineWay` diff --git a/tests/integration/repo_git_note_test.go b/tests/integration/repo_git_note_test.go index e6b23754db..34c22ae3c0 100644 --- a/tests/integration/repo_git_note_test.go +++ b/tests/integration/repo_git_note_test.go @@ -11,7 +11,7 @@ import ( ) func TestRepoModifyGitNotes(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { session := loginUser(t, "user2") req := NewRequest(t, "GET", "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d") @@ -46,7 +46,7 @@ func TestRepoModifyGitNotes(t *testing.T) { } func TestRepoGitNotesButtonsVisible(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { + onApplicationRun(t, func(*testing.T, *url.URL) { t.Run("With Permission", func(t *testing.T) { defer tests.PrintCurrentTest(t)() diff --git a/tests/integration/repo_issue_title_test.go b/tests/integration/repo_issue_title_test.go index 587db43223..a722ef5610 100644 --- a/tests/integration/repo_issue_title_test.go +++ b/tests/integration/repo_issue_title_test.go @@ -26,7 +26,7 @@ import ( ) func TestIssueTitles(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) repo, _, f := tests.CreateDeclarativeRepo(t, user, "issue-titles", nil, nil, nil) defer f() diff --git a/tests/integration/repo_migrate_credentials_test.go b/tests/integration/repo_migrate_credentials_test.go index b63ca9b29e..ed0d798ec7 100644 --- a/tests/integration/repo_migrate_credentials_test.go +++ b/tests/integration/repo_migrate_credentials_test.go @@ -21,7 +21,7 @@ import ( ) func TestRepoMigrateWithCredentials(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)() require.NoError(t, migrations.Init()) diff --git a/tests/integration/repo_settings_test.go b/tests/integration/repo_settings_test.go index 6b467d78b2..e3a1dede4d 100644 --- a/tests/integration/repo_settings_test.go +++ b/tests/integration/repo_settings_test.go @@ -16,7 +16,7 @@ import ( user_model "forgejo.org/models/user" "forgejo.org/modules/optional" "forgejo.org/modules/setting" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" repo_service "forgejo.org/services/repository" user_service "forgejo.org/services/user" "forgejo.org/tests" @@ -298,7 +298,7 @@ func TestProtectedBranch(t *testing.T) { "require_signed_": "true", }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Equal(t, "error%3DThere%2Bis%2Balready%2Ba%2Brule%2Bfor%2Bthis%2Bset%2Bof%2Bbranches", flashCookie.Value) diff --git a/tests/integration/repo_sync_fork_test.go b/tests/integration/repo_sync_fork_test.go index fb08a69d8b..46730641ce 100644 --- a/tests/integration/repo_sync_fork_test.go +++ b/tests/integration/repo_sync_fork_test.go @@ -77,25 +77,25 @@ func syncForkTest(t *testing.T, forkName, branchName string, webSync bool) { } func TestAPIRepoSyncForkDefault(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { syncForkTest(t, "SyncForkDefault", "master", false) }) } func TestAPIRepoSyncForkBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { syncForkTest(t, "SyncForkBranch", "master", false) }) } func TestWebRepoSyncForkBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { syncForkTest(t, "SyncForkBranch", "master", true) }) } func TestWebRepoSyncForkHomepage(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) baseOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID}) baseOwnerSession := loginUser(t, baseOwner.Name) diff --git a/tests/integration/repo_tag_test.go b/tests/integration/repo_tag_test.go index b119f5d917..972a51089b 100644 --- a/tests/integration/repo_tag_test.go +++ b/tests/integration/repo_tag_test.go @@ -86,7 +86,7 @@ func TestTagViewWithoutRelease(t *testing.T) { } func TestCreateNewTagProtected(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -155,7 +155,7 @@ func TestCreateNewTagProtected(t *testing.T) { } func TestSyncRepoTags(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) @@ -193,7 +193,7 @@ func TestSyncRepoTags(t *testing.T) { } func TestRepushTag(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, owner.LowerName) diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index f8b926e8be..bd5900070d 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -722,7 +722,7 @@ func TestViewCommitSignature(t *testing.T) { fromBranch := "master" toBranch := "branch-signed" - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { // Use a new GNUPGPHOME to avoid messing with the existing GPG keyring. tmpDir := t.TempDir() require.NoError(t, os.Chmod(tmpDir, 0o700)) @@ -931,7 +931,7 @@ func TestRepoHomeViewRedirect(t *testing.T) { } func TestRepoFilesList(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // create the repo @@ -1492,7 +1492,7 @@ func TestRepoIssueFilterLinks(t *testing.T) { } func TestRepoSubmoduleView(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo, _, f := tests.CreateDeclarativeRepo(t, user2, "", []unit_model.Type{unit_model.TypeCode}, nil, nil) defer f() diff --git a/tests/integration/repo_view_test.go b/tests/integration/repo_view_test.go index 7ea4aeb4c6..ce24aca16a 100644 --- a/tests/integration/repo_view_test.go +++ b/tests/integration/repo_view_test.go @@ -62,7 +62,7 @@ func createRepoAndGetContext(t *testing.T, files []string, deleteMdReadme bool) } func TestRepoView_FindReadme(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { t.Run("PrioOneLocalizedMdReadme", func(t *testing.T) { defer tests.PrintCurrentTest(t)() ctx, f := createRepoAndGetContext(t, []string{"README.en.md", "README.en.org", "README.org", "README.txt", "README.tex"}, false) @@ -155,7 +155,7 @@ func TestRepoView_FindReadme(t *testing.T) { } func TestRepoViewFileLines(t *testing.T) { - onGiteaRun(t, func(t *testing.T, _ *url.URL) { + onApplicationRun(t, func(t *testing.T, _ *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo, _, f := tests.CreateDeclarativeRepo(t, user, "file-lines", []unit_model.Type{unit_model.TypeCode}, nil, []*files_service.ChangeRepoFile{ { diff --git a/tests/integration/repo_watch_test.go b/tests/integration/repo_watch_test.go index 0bcb039b01..8d6af5b381 100644 --- a/tests/integration/repo_watch_test.go +++ b/tests/integration/repo_watch_test.go @@ -13,7 +13,7 @@ import ( ) func TestRepoWatch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { // Test round-trip auto-watch setting.Service.AutoWatchOnChanges = true session := loginUser(t, "user2") diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index ffddd47faa..aa2d7538a1 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -9,7 +9,7 @@ import ( "net/url" "testing" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" "forgejo.org/services/webhook" "forgejo.org/tests" @@ -447,7 +447,7 @@ func assertHasFlashMessages(t *testing.T, resp *httptest.ResponseRecorder, expec seenKeys := make(map[string][]string, len(expectedKeys)) for _, cookie := range resp.Result().Cookies() { - if cookie.Name != gitea_context.CookieNameFlash { + if cookie.Name != app_context.CookieNameFlash { continue } flash, _ := url.ParseQuery(cookie.Value) diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index 42d47c4591..1c3f86d001 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -250,7 +250,7 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA } func TestChangeRepoFiles(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -414,7 +414,7 @@ func TestChangeRepoFiles(t *testing.T) { func TestChangeRepoFilesErrors(t *testing.T) { // setup - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) diff --git a/tests/integration/signing_git_test.go b/tests/integration/signing_git_test.go index 7018b10376..7fbda358cf 100644 --- a/tests/integration/signing_git_test.go +++ b/tests/integration/signing_git_test.go @@ -35,7 +35,7 @@ func TestInstanceSigning(t *testing.T) { require.NoError(t, git.InitFull(context.Background())) }) - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "UwU")() defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "fox@example.com")() defer test.MockProtect(&setting.Repository.Signing.InitialCommit)() diff --git a/tests/integration/ssh_key_test.go b/tests/integration/ssh_key_test.go index a92694d2fa..156bcb137e 100644 --- a/tests/integration/ssh_key_test.go +++ b/tests/integration/ssh_key_test.go @@ -44,7 +44,7 @@ func doAddChangesToCheckout(dstPath, filename string) func(*testing.T) { } func TestPushDeployKeyOnEmptyRepo(t *testing.T) { - onGiteaRun(t, testPushDeployKeyOnEmptyRepo) + onApplicationRun(t, testPushDeployKeyOnEmptyRepo) } func testPushDeployKeyOnEmptyRepo(t *testing.T, u *url.URL) { @@ -88,7 +88,7 @@ func testPushDeployKeyOnEmptyRepo(t *testing.T, u *url.URL) { } func TestKeyOnlyOneType(t *testing.T) { - onGiteaRun(t, testKeyOnlyOneType) + onApplicationRun(t, testKeyOnlyOneType) } func testKeyOnlyOneType(t *testing.T, u *url.URL) { diff --git a/tests/integration/translations_test.go b/tests/integration/translations_test.go index 7b80025618..d20355aca2 100644 --- a/tests/integration/translations_test.go +++ b/tests/integration/translations_test.go @@ -44,7 +44,7 @@ func TestMissingTranslationHandling(t *testing.T) { // TestDataSizeTranslation is a test for usage of TrSize in file size display func TestDataSizeTranslation(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { testUser := "user2" testRepoName := "data_size_test" noDigits := regexp.MustCompile("[0-9]+") diff --git a/tests/integration/user_dashboard_test.go b/tests/integration/user_dashboard_test.go index fefe5f1399..146358158d 100644 --- a/tests/integration/user_dashboard_test.go +++ b/tests/integration/user_dashboard_test.go @@ -45,7 +45,7 @@ func testUserDashboardFeedType(t *testing.T, page *HTMLDoc, isEmpty bool) { } func TestDashboardTitleRendering(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) sess := loginUser(t, user4.Name) @@ -91,7 +91,7 @@ func TestDashboardTitleRendering(t *testing.T) { } func TestDashboardActionEscaping(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) sess := loginUser(t, user4.Name) @@ -126,7 +126,7 @@ func TestDashboardActionEscaping(t *testing.T) { } func TestDashboardReviewWorkflows(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) sess := loginUser(t, user4.Name) diff --git a/tests/integration/user_profile_test.go b/tests/integration/user_profile_test.go index 0e3668f1d5..654ff0c094 100644 --- a/tests/integration/user_profile_test.go +++ b/tests/integration/user_profile_test.go @@ -11,16 +11,19 @@ import ( "forgejo.org/models/unittest" user_model "forgejo.org/models/user" + "forgejo.org/modules/git" "forgejo.org/modules/setting" "forgejo.org/modules/test" + repo_service "forgejo.org/services/repository" files_service "forgejo.org/services/repository/files" "forgejo.org/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUserProfile(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { checkReadme := func(t *testing.T, title, readmeFilename string, expectedCount int) { t.Run(title, func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -112,5 +115,60 @@ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequa assert.NotContains(t, resp.Body.String(), "veniam") }) }) + + t.Run("forked-profile-repo", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Create users + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + // Create original .profile repository for user2 + originalRepo, _, f1 := tests.CreateDeclarativeRepo(t, user2, ".profile", nil, nil, []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: "README.md", + ContentReader: strings.NewReader("# Original Profile Content\nThis should show up on user2 profile."), + }, + }) + defer f1() + + // Fork the .profile repository to user4 + forkedRepo, err := repo_service.ForkRepositoryAndUpdates(git.DefaultContext, user2, user4, repo_service.ForkRepoOptions{ + BaseRepo: originalRepo, + Name: ".profile", + }) + require.NoError(t, err) + + // Verify that user2's profile shows the original content + req := NewRequest(t, "GET", "/user2") + resp := MakeRequest(t, req, http.StatusOK) + // Check if the content appears in the response body + bodyStr := resp.Body.String() + if strings.Contains(bodyStr, "Original Profile Content") { + // Original profile is working correctly + assert.Contains(t, bodyStr, "This should show up on user2 profile", "Original profile should render content") + } + + // Verify that user4's profile does NOT show the forked content + // Since it's a fork, it should not render as a profile page (this is the main test) + req = NewRequest(t, "GET", "/user4") + resp = MakeRequest(t, req, http.StatusOK) + bodyStr = resp.Body.String() + + // The main assertion: forked .profile content should NOT appear on user profile + assert.NotContains(t, bodyStr, "Original Profile Content", "Forked .profile repo should NOT render profile content") + assert.NotContains(t, bodyStr, "This should show up on user2 profile", "Forked .profile repo should NOT render profile content") + + // Ensure the forked repository still exists and is accessible directly + req = NewRequest(t, "GET", "/user4/.profile") + resp = MakeRequest(t, req, http.StatusOK) + // The repository page should show the content (since it's the same as original) + assert.Contains(t, resp.Body.String(), "Original Profile Content", "Forked repo should still be accessible") + + // Verify the fork relationship + assert.True(t, forkedRepo.IsFork, "Repository should be marked as a fork") + assert.Equal(t, originalRepo.ID, forkedRepo.ForkID, "Fork should reference original repository") + }) }) } diff --git a/tests/integration/user_recovery_test.go b/tests/integration/user_recovery_test.go new file mode 100644 index 0000000000..94114c6b38 --- /dev/null +++ b/tests/integration/user_recovery_test.go @@ -0,0 +1,52 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package integration + +import ( + "net/http" + "testing" + + "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + "forgejo.org/modules/test" + "forgejo.org/modules/translation" + "forgejo.org/services/mailer" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" +) + +func TestForgotPassword(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + test := func(t *testing.T, user *user_model.User, email *user_model.EmailAddress) { + t.Helper() + + called := false + defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) { + assert.Len(t, msgs, 1) + assert.Equal(t, user.EmailTo(), msgs[0].To) + assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.reset_password"), msgs[0].Subject) + assert.Contains(t, msgs[0].Body, translation.NewLocale("en-US").Tr("mail.reset_password.text", "3 hours")) + called = true + })() + + req := NewRequestWithValues(t, "POST", "/user/forgot_password", map[string]string{ + "_csrf": GetCSRF(t, emptyTestSession(t), "/user/forgot_password"), + "email": email.Email, + }) + MakeRequest(t, req, http.StatusOK) + + assert.True(t, called) + } + t.Run("Unactivated email address", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + test(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 11}), unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{UID: 11}, "is_activated = false")) + }) + + t.Run("Activated email address", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + test(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12}), unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{UID: 12}, "is_activated = true")) + }) +} diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index fd5c85e970..0e993636c7 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -26,7 +26,7 @@ import ( api "forgejo.org/modules/structs" "forgejo.org/modules/test" "forgejo.org/modules/translation" - gitea_context "forgejo.org/services/context" + app_context "forgejo.org/services/context" "forgejo.org/services/mailer" "forgejo.org/tests" @@ -917,7 +917,7 @@ func TestUserTOTPReenroll(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) assert.Contains(t, flashCookie.Value, "success%3DYour%2Baccount%2Bhas%2Bbeen%2Bsuccessfully%2Benrolled.") } @@ -945,7 +945,7 @@ func TestUserTOTPDisable(t *testing.T) { session.MakeRequest(t, req, status) } if flashMessage != "" { - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + flashCookie := session.GetCookie(app_context.CookieNameFlash) assert.NotNil(t, flashCookie) if disableAllowed { assert.Contains(t, flashCookie.Value, fmt.Sprintf("success%%3D%s", flashMessage)) diff --git a/tests/integration/view_test.go b/tests/integration/view_test.go index 80371b84ce..0a210a5155 100644 --- a/tests/integration/view_test.go +++ b/tests/integration/view_test.go @@ -36,7 +36,7 @@ func TestRenderFileSVGIsInImgTag(t *testing.T) { } func TestAmbiguousCharacterDetection(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) @@ -132,7 +132,7 @@ func TestAmbiguousCharacterDetection(t *testing.T) { } func TestCommitListActions(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, user2.Name) repo, commitID, f := tests.CreateDeclarativeRepo(t, user2, "", diff --git a/tests/integration/webhook_test.go b/tests/integration/webhook_test.go index 6d92a16dd8..3eef54f50c 100644 --- a/tests/integration/webhook_test.go +++ b/tests/integration/webhook_test.go @@ -25,7 +25,7 @@ import ( ) func TestWebhookPayloadRef(t *testing.T) { - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + onApplicationRun(t, func(t *testing.T, giteaURL *url.URL) { w := unittest.AssertExistsAndLoadBean(t, &webhook_model.Webhook{ID: 1}) w.HookEvent = &webhook_module.HookEvent{ SendEverything: true, diff --git a/tests/integration/wiki_test.go b/tests/integration/wiki_test.go index 2949038f1f..47986177d7 100644 --- a/tests/integration/wiki_test.go +++ b/tests/integration/wiki_test.go @@ -34,7 +34,7 @@ func assertFileEqual(t *testing.T, p string, content []byte) { } func TestRepoCloneWiki(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { + onApplicationRun(t, func(t *testing.T, u *url.URL) { dstPath := t.TempDir() r := fmt.Sprintf("%suser2/repo1.wiki.git", u.String()) diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl index 055cc091c5..e50d5c4792 100644 --- a/tests/mysql.ini.tmpl +++ b/tests/mysql.ini.tmpl @@ -125,3 +125,11 @@ ENABLED = false [cron.check_repo_stats] ENABLED = false + +# For iframe rendering tests +[markup.iframehtml] +ENABLED = true +FILE_EXTENSIONS = .iframehtml +RENDER_COMMAND = cat +RENDER_CONTENT_MODE = iframe +NEED_POSTPROCESS = false diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl index 0293817c6e..4c494941c3 100644 --- a/tests/pgsql.ini.tmpl +++ b/tests/pgsql.ini.tmpl @@ -139,3 +139,11 @@ ENABLED = false [cron.check_repo_stats] ENABLED = false + +# For iframe rendering tests +[markup.iframehtml] +ENABLED = true +FILE_EXTENSIONS = .iframehtml +RENDER_COMMAND = cat +RENDER_CONTENT_MODE = iframe +NEED_POSTPROCESS = false diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 6a08bce8ea..0514c684e6 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -126,3 +126,11 @@ ENABLED = false [cron.check_repo_stats] ENABLED = false + +# For iframe rendering tests +[markup.iframehtml] +ENABLED = true +FILE_EXTENSIONS = .iframehtml +RENDER_COMMAND = cat +RENDER_CONTENT_MODE = iframe +NEED_POSTPROCESS = false diff --git a/web_src/css/admin.css b/web_src/css/admin.css index e6866b27a6..873b1bf91e 100644 --- a/web_src/css/admin.css +++ b/web_src/css/admin.css @@ -29,8 +29,8 @@ min-width: 100px; } -.admin code, -.admin pre { +:is(.admin, #detail-modal) code, +:is(.admin, #detail-modal) pre { white-space: pre-wrap; word-wrap: break-word; } diff --git a/web_src/css/base.css b/web_src/css/base.css index af23ecf2c2..b6c0b9cf49 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1,6 +1,6 @@ :root { /* fonts */ - --fonts-proportional: -apple-system, "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial; + --fonts-proportional: -apple-system, "Segoe UI", system-ui, "Noto Sans", "Noto Sans Hebrew", Roboto, "Helvetica Neue", Arial; --fonts-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace, var(--fonts-emoji); --fonts-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla"; /* font weights - use between 400 and 600 for general purposes. Avoid 700 as it is perceived too bold */ @@ -50,7 +50,7 @@ } :root * { - --fonts-regular: var(--fonts-override, var(--fonts-proportional)), "Noto Sans", "Liberation Sans", sans-serif, var(--fonts-emoji); + --fonts-regular: var(--fonts-override, var(--fonts-proportional)), "Liberation Sans", sans-serif, var(--fonts-emoji); } *, ::before, ::after { @@ -265,28 +265,6 @@ h1.error-code { user-select: none; } -.button-row { - gap: var(--button-spacing); -} - -.button-sequence { - display: flex; - flex-flow: wrap; - gap: var(--button-spacing); -} - -.button-sequence.right { - justify-content: end; -} - -.button-sequence .ui.button { - margin: 0 !important; -} - -.button-row .ui.button { - margin-right: 0; -} - .ui.partial.secondary.menu { margin-bottom: 0; } @@ -671,11 +649,11 @@ img.ui.avatar, } .text.red { - color: var(--color-red) !important; + color: var(--color-thin-red, var(--color-red)) !important; } .text.orange { - color: var(--color-orange) !important; + color: var(--color-thin-orange, var(--color-orange)) !important; } .text.yellow { @@ -683,7 +661,7 @@ img.ui.avatar, } .text.green { - color: var(--color-green) !important; + color: var(--color-thin-green, var(--color-green)) !important; } .text.teal { @@ -695,7 +673,7 @@ img.ui.avatar, } .text.purple { - color: var(--color-purple) !important; + color: var(--color-thin-purple, var(--color-purple)) !important; } .text.brown { @@ -1023,20 +1001,6 @@ overflow-menu .ui.label { color: var(--color-secondary-dark-2) !important; } -/* colors of colorful icons */ -svg.text.green, -.text.green svg { - color: var(--color-icon-green) !important; -} -svg.text.red, -.text.red svg { - color: var(--color-icon-red) !important; -} -svg.text.purple, -.text.purple svg { - color: var(--color-icon-purple) !important; -} - .oauth2-authorize-application-box { margin-top: 3em !important; } diff --git a/web_src/css/editor/combomarkdowneditor.css b/web_src/css/editor/combomarkdowneditor.css index b151080c64..77af4cb87b 100644 --- a/web_src/css/editor/combomarkdowneditor.css +++ b/web_src/css/editor/combomarkdowneditor.css @@ -11,12 +11,8 @@ flex-wrap: wrap; } -.markdown-toolbar-switch { - display: flex; - height: 30px; -} -.markdown-toolbar-switch .switch .item { - padding: 0.25em 1em; +:root markdown-toolbar .switch { + --switch-item-min-height: 2rem; } .markdown-toolbar-hidden .markdown-toolbar-button { @@ -84,7 +80,7 @@ } text-expander { - display: block; + display: flex; position: relative; } diff --git a/web_src/css/index.css b/web_src/css/index.css index 8cb25d8185..feb84f1cf7 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -2,7 +2,7 @@ @import "./modules/animations.css"; /* fomantic replacements */ -@import "./modules/button.css"; +@import "./modules/button-legacy.css"; @import "./modules/container.css"; @import "./modules/divider.css"; @import "./modules/header.css"; @@ -20,6 +20,7 @@ @import "./modules/dimmer.css"; @import "./modules/dialog.css"; +@import "./modules/button.css"; @import "./modules/switch.css"; @import "./modules/dropdown.css"; @import "./modules/select.css"; @@ -32,6 +33,7 @@ @import "./modules/flexcontainer.css"; @import "./modules/user-cards.css"; @import "./modules/hashbox.css"; +@import "./modules/stats-bar.css"; @import "./shared/flex-list.css"; @import "./shared/milestone.css"; diff --git a/web_src/css/modules/button-legacy.css b/web_src/css/modules/button-legacy.css new file mode 100644 index 0000000000..469a309a11 --- /dev/null +++ b/web_src/css/modules/button-legacy.css @@ -0,0 +1,592 @@ +/* this contains override styles for buttons and related elements */ + +/* these styles changed the Fomantic UI's rules, Fomantic UI expects only "basic" buttons have borders */ +.ui.button { + background: var(--color-button); + border: 1px solid var(--color-light-border); + color: var(--color-text); +} + +.ui.button:hover, +.ui.button:focus { + background: var(--color-hover); + color: var(--color-text); +} + +.page-content .ui.button { + box-shadow: none !important; +} + +.ui.active.button, +.ui.button:active, +.ui.active.button:active, +.ui.active.button:hover, +.ui.active.button:focus { + background: var(--color-active); + color: var(--color-text); +} + +.delete-button, +.delete-button:hover, +.delete-button:focus { + color: var(--color-red); +} + +/* btn is a plain button without any opinionated styling, it only uses flex for vertical alignment like ".ui.button" in base.css */ + +.btn { + background: transparent; + border-radius: var(--border-radius); + border: none; + color: inherit; + margin: 0; + padding: 0; +} + +.btn:hover, +.btn:active, +.btn:focus { + background: none; + border: none; +} + +a.btn, +a.btn:hover { + color: inherit; +} + +/* By default, Fomantic UI doesn't support "bordered" buttons group, but Gitea would like to use it. +And the default buttons always have borders now (not the same as Fomantic UI's default buttons, see above). +It needs some tricks to tweak the left/right borders with active state */ + +.ui.buttons .button { + border-right: none; +} + +.ui.buttons .button:hover { + border-color: var(--color-secondary-dark-2); +} + +.ui.buttons .button:hover + .button { + border-left: 1px solid var(--color-secondary-dark-2); +} + +/* TODO: these "tw-hidden" selectors are only used by "blame.tmpl" buttons: Raw/Normal View/History/Unescape, need to be refactored to a clear solution later */ +.ui.buttons .button:first-child, +.ui.buttons .button.tw-hidden:first-child + .button { + border-left: 1px solid var(--color-light-border); +} + +.ui.buttons .button:last-child, +.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden) { + border-right: 1px solid var(--color-light-border); +} + +.ui.buttons .button.active { + border-left: 1px solid var(--color-light-border); + border-right: 1px solid var(--color-light-border); +} + +.ui.buttons .button.active + .button { + border-left: none; +} + +.ui.basic.buttons .button, +.ui.basic.button, +.ui.basic.buttons .button:hover, +.ui.basic.button:hover { + box-shadow: none; +} + +/* apply the vertical padding of .compact to non-compact buttons when they contain a svg as they + would otherwise appear too large. Seen on "RSS Feed" button on repo releases tab. */ +.ui.small.button:not(.compact):has(.svg) { + padding-top: 0.58928571em; + padding-bottom: 0.58928571em; +} + +.ui.labeled.button.disabled > .button, +.ui.basic.buttons .button, +.ui.basic.button { + color: var(--color-text-light); + background: var(--color-button); +} + +.ui.basic.buttons .button:hover, +.ui.basic.button:hover, +.ui.basic.buttons .button:focus, +.ui.basic.button:focus { + color: var(--color-text); + background: var(--color-hover); + border-color: var(--color-secondary-dark-2); +} + +.ui.basic.buttons .button:active, +.ui.basic.button:active, +.ui.basic.buttons .active.button, +.ui.basic.active.button, +.ui.basic.buttons .active.button:hover, +.ui.basic.active.button:hover, +.ui.basic.buttons .active.button:focus, +.ui.basic.active.button:focus { + color: var(--color-text); + background: var(--color-active); +} + +.ui.labeled.button > .label { + border-color: var(--color-light-border); +} + +.ui.labeled.icon.buttons > .button > .icon, +.ui.labeled.icon.button > .icon { + background: var(--color-hover); +} + +/* primary */ + +.ui.primary.labels .label, +.ui.ui.ui.primary.label, +.ui.primary.button, +.ui.primary.buttons .button { + background: var(--color-primary); + color: var(--color-primary-contrast); +} + +.ui.primary.button:hover, +.ui.primary.buttons .button:hover, +.ui.primary.button:focus, +.ui.primary.buttons .button:focus { + background: var(--color-primary-hover); + color: var(--color-primary-contrast); +} + +.ui.primary.button:active, +.ui.primary.buttons .button:active { + background: var(--color-primary-active); +} + +.ui.basic.primary.buttons .button, +.ui.basic.primary.button { + color: var(--color-primary); + border-color: var(--color-primary); +} + +.ui.basic.primary.buttons .button:hover, +.ui.basic.primary.button:hover, +.ui.basic.primary.buttons .button:focus, +.ui.basic.primary.button:focus { + color: var(--color-primary-hover); + border-color: var(--color-primary-hover); +} + +.ui.basic.primary.buttons .button:active, +.ui.basic.primary.button:active { + color: var(--color-primary-active); + border-color: var(--color-primary-active); +} + +/* secondary */ + +.ui.secondary.labels .label, +.ui.ui.ui.secondary.label, +.ui.secondary.button, +.ui.secondary.buttons .button, +.ui.secondary.button:focus, +.ui.secondary.buttons .button:focus { + background: var(--color-secondary-button); +} + +.ui.secondary.button:hover, +.ui.secondary.buttons .button:hover { + background: var(--color-secondary-hover); +} + +.ui.secondary.button:active, +.ui.secondary.buttons .button:active { + background: var(--color-secondary-active); +} + +.ui.basic.secondary.buttons .button, +.ui.basic.secondary.button { + color: var(--color-secondary-button); + border-color: var(--color-secondary-button); +} + +.ui.basic.secondary.buttons .button:hover, +.ui.basic.secondary.button:hover, +.ui.basic.secondary.button:focus, +.ui.basic.secondary.buttons .button:focus { + color: var(--color-secondary-hover); + border-color: var(--color-secondary-hover); +} + +.ui.basic.secondary.buttons .button:active, +.ui.basic.secondary.button:active { + color: var(--color-secondary-active); + border-color: var(--color-secondary-active); +} + +/* red */ + +.ui.red.labels .label, +.ui.ui.ui.red.label, +.ui.red.button, +.ui.red.buttons .button { + background: var(--color-red); +} + +.ui.red.button:hover, +.ui.red.buttons .button:hover, +.ui.red.button:focus, +.ui.red.buttons .button:focus { + background: var(--color-red-dark-1); +} + +.ui.red.button:active, +.ui.red.buttons .button:active { + background: var(--color-red-dark-2); +} + +.ui.basic.red.buttons .button, +.ui.basic.red.button { + color: var(--color-red); + border-color: var(--color-red); +} + +.ui.basic.red.buttons .button:hover, +.ui.basic.red.button:hover, +.ui.basic.red.buttons .button:focus, +.ui.basic.red.button:focus { + color: var(--color-red-dark-1); + border-color: var(--color-red-dark-1); +} + +.ui.basic.red.buttons .button:active, +.ui.basic.red.button:active { + color: var(--color-red-dark-2); + border-color: var(--color-red-dark-2); +} + +/* orange */ + +.ui.orange.labels .label, +.ui.ui.ui.orange.label, +.ui.orange.button, +.ui.orange.buttons .button, +.ui.orange.button:focus, +.ui.orange.buttons .button:focus { + background: var(--color-orange); +} + +.ui.orange.button:hover, +.ui.orange.buttons .button:hover { + background: var(--color-orange-dark-1); +} + +.ui.orange.button:active, +.ui.orange.buttons .button:active { + background: var(--color-orange-dark-2); +} + +.ui.basic.orange.buttons .button, +.ui.basic.orange.button, +.ui.basic.orange.buttons .button:focus, +.ui.basic.orange.button:focus { + color: var(--color-orange); + border-color: var(--color-orange); +} + +.ui.basic.orange.buttons .button:hover, +.ui.basic.orange.button:hover { + color: var(--color-orange-dark-1); + border-color: var(--color-orange-dark-1); +} + +.ui.basic.orange.buttons .button:active, +.ui.basic.orange.button:active { + color: var(--color-orange-dark-2); + border-color: var(--color-orange-dark-2); +} + +/* yellow */ + +.ui.yellow.labels .label, +.ui.ui.ui.yellow.label, +.ui.yellow.button, +.ui.yellow.buttons .button, +.ui.yellow.button:focus, +.ui.yellow.buttons .button:focus { + background: var(--color-yellow); +} + +.ui.yellow.button:hover, +.ui.yellow.buttons .button:hover { + background: var(--color-yellow-dark-1); +} + +.ui.yellow.button:active, +.ui.yellow.buttons .button:active { + background: var(--color-yellow-dark-2); +} + +.ui.basic.yellow.buttons .button, +.ui.basic.yellow.button, +.ui.basic.yellow.buttons .button:focus, +.ui.basic.yellow.button:focus { + color: var(--color-yellow); + border-color: var(--color-yellow); +} + +.ui.basic.yellow.buttons .button:hover, +.ui.basic.yellow.button:hover { + color: var(--color-yellow-dark-1); + border-color: var(--color-yellow-dark-1); +} + +.ui.basic.yellow.buttons .button:active, +.ui.basic.yellow.button:active { + color: var(--color-yellow-dark-2); + border-color: var(--color-yellow-dark-2); +} + +/* green */ + +.ui.green.labels .label, +.ui.ui.ui.green.label, +.ui.green.button, +.ui.green.buttons .button, +.ui.green.button:focus, +.ui.green.buttons .button:focus { + background: var(--color-green); +} + +.ui.green.button:hover, +.ui.green.buttons .button:hover { + background: var(--color-green-dark-1); +} + +.ui.green.button:active, +.ui.green.buttons .button:active { + background: var(--color-green-dark-2); +} + +.ui.basic.green.buttons .button, +.ui.basic.green.button, +.ui.basic.green.buttons .button:focus, +.ui.basic.green.button:focus { + color: var(--color-green); + border-color: var(--color-green); +} + +.ui.basic.green.buttons .button:hover, +.ui.basic.green.button:hover { + color: var(--color-green-dark-1); + border-color: var(--color-green-dark-1); +} + +.ui.basic.green.buttons .button:active, +.ui.basic.green.button:active { + color: var(--color-green-dark-2); + border-color: var(--color-green-dark-2); +} + +/* teal */ + +.ui.teal.labels .label, +.ui.ui.ui.teal.label, +.ui.teal.button, +.ui.teal.buttons .button, +.ui.teal.button:focus, +.ui.teal.buttons .button:focus { + background: var(--color-teal); +} + +.ui.teal.button:hover, +.ui.teal.buttons .button:hover { + background: var(--color-teal-dark-1); +} + +.ui.teal.button:active, +.ui.teal.buttons .button:active { + background: var(--color-teal-dark-2); +} + +.ui.basic.teal.buttons .button, +.ui.basic.teal.button, +.ui.basic.teal.buttons .button:focus, +.ui.basic.teal.button:focus { + color: var(--color-teal); + border-color: var(--color-teal); +} + +.ui.basic.teal.buttons .button:hover, +.ui.basic.teal.button:hover { + color: var(--color-teal-dark-1); + border-color: var(--color-teal-dark-1); +} + +.ui.basic.teal.buttons .button:active, +.ui.basic.teal.button:active { + color: var(--color-teal-dark-2); + border-color: var(--color-teal-dark-2); +} + +/* purple */ + +.ui.purple.labels .label, +.ui.ui.ui.purple.label, +.ui.purple.button, +.ui.purple.buttons .button, +.ui.purple.button:focus, +.ui.purple.buttons .button:focus { + background: var(--color-purple); +} + +.ui.purple.button:hover, +.ui.purple.buttons .button:hover { + background: var(--color-purple-dark-1); +} + +.ui.purple.button:active, +.ui.purple.buttons .button:active { + background: var(--color-purple-dark-2); +} + +.ui.basic.purple.buttons .button, +.ui.basic.purple.button, +.ui.basic.purple.buttons .button:focus, +.ui.basic.purple.button:focus { + color: var(--color-purple); + border-color: var(--color-purple); +} + +.ui.basic.purple.buttons .button:hover, +.ui.basic.purple.button:hover { + color: var(--color-purple-dark-1); + border-color: var(--color-purple-dark-1); +} + +.ui.basic.purple.buttons .button:active, +.ui.basic.purple.button:active { + color: var(--color-purple-dark-2); + border-color: var(--color-purple-dark-2); +} + +/* brown */ + +.ui.brown.labels .label, +.ui.ui.ui.brown.label, +.ui.brown.button, +.ui.brown.buttons .button, +.ui.brown.button:focus, +.ui.brown.buttons .button:focus { + background: var(--color-brown); +} + +.ui.brown.button:hover, +.ui.brown.buttons .button:hover { + background: var(--color-brown-dark-1); +} + +.ui.brown.button:active, +.ui.brown.buttons .button:active { + background: var(--color-brown-dark-2); +} + +.ui.basic.brown.buttons .button, +.ui.basic.brown.button, +.ui.basic.brown.buttons .button:focus, +.ui.basic.brown.button:focus { + color: var(--color-brown); + border-color: var(--color-brown); +} + +.ui.basic.brown.buttons .button:hover, +.ui.basic.brown.button:hover { + color: var(--color-brown-dark-1); + border-color: var(--color-brown-dark-1); +} + +.ui.basic.brown.buttons .button:active, +.ui.basic.brown.button:active { + color: var(--color-brown-dark-2); + border-color: var(--color-brown-dark-2); +} + +/* negative */ + +.ui.negative.buttons .button, +.ui.negative.button, +.ui.negative.buttons .button:focus, +.ui.negative.button:focus { + background: var(--color-red); +} + +.ui.negative.buttons .button:hover, +.ui.negative.button:hover { + background: var(--color-red-dark-1); +} + +.ui.negative.buttons .button:active, +.ui.negative.button:active { + background: var(--color-red-dark-2); +} + +.ui.basic.negative.buttons .button, +.ui.basic.negative.button, +.ui.basic.negative.buttons .button:focus, +.ui.basic.negative.button:focus { + color: var(--color-red); + border-color: var(--color-red); +} + +.ui.basic.negative.buttons .button:hover, +.ui.basic.negative.button:hover { + color: var(--color-red-dark-1); + border-color: var(--color-red-dark-1); +} + +.ui.basic.negative.buttons .button:active, +.ui.basic.negative.button:active { + color: var(--color-red-dark-2); + border-color: var(--color-red-dark-2); +} + +/* positive */ + +.ui.positive.buttons .button, +.ui.positive.button, +.ui.positive.buttons .button:focus, +.ui.positive.button:focus { + background: var(--color-green); +} + +.ui.positive.buttons .button:hover, +.ui.positive.button:hover { + background: var(--color-green-dark-1); +} + +.ui.positive.buttons .button:active, +.ui.positive.button:active { + background: var(--color-green-dark-2); +} + +.ui.basic.positive.buttons .button, +.ui.basic.positive.button, +.ui.basic.positive.buttons .button:focus, +.ui.basic.positive.button:focus { + color: var(--color-green); + border-color: var(--color-green); +} + +.ui.basic.positive.buttons .button:hover, +.ui.basic.positive.button:hover { + color: var(--color-green-dark-1); + border-color: var(--color-green-dark-1); +} + +.ui.basic.positive.buttons .button:active, +.ui.basic.positive.button:active { + color: var(--color-green-dark-2); + border-color: var(--color-green-dark-2); +} diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css index 469a309a11..5ebc699c99 100644 --- a/web_src/css/modules/button.css +++ b/web_src/css/modules/button.css @@ -1,592 +1,94 @@ -/* this contains override styles for buttons and related elements */ +/* Copyright 2024-2025 The Forgejo Authors. All rights reserved. + * SPDX-License-Identifier: GPL-3.0-or-later */ -/* these styles changed the Fomantic UI's rules, Fomantic UI expects only "basic" buttons have borders */ -.ui.button { - background: var(--color-button); - border: 1px solid var(--color-light-border); - color: var(--color-text); +:root { + --button-min-height: 36px; + --button-padding-inline: 1.35rem; } -.ui.button:hover, -.ui.button:focus { - background: var(--color-hover); - color: var(--color-text); +:root .small.button, +:root .small.button-row, +:root .small.button-sequence { + --button-min-height: 34px; + --button-padding-inline: 1.25rem; } -.page-content .ui.button { - box-shadow: none !important; +@media (pointer: coarse) { + :root { + --button-min-height: 40px; + } + + :root .small.button, + :root .small.button-row, + :root .small.button-sequence { + --button-min-height: 38px; + } } -.ui.active.button, -.ui.button:active, -.ui.active.button:active, -.ui.active.button:hover, -.ui.active.button:focus { - background: var(--color-active); - color: var(--color-text); -} - -.delete-button, -.delete-button:hover, -.delete-button:focus { - color: var(--color-red); -} - -/* btn is a plain button without any opinionated styling, it only uses flex for vertical alignment like ".ui.button" in base.css */ - -.btn { - background: transparent; +.button { + display: inline-flex; + align-items: center; + min-height: var(--button-min-height); + padding-block: 0; + padding-inline: var(--button-padding-inline); + justify-content: center; + gap: 0.5rem; border-radius: var(--border-radius); - border: none; - color: inherit; - margin: 0; - padding: 0; } -.btn:hover, -.btn:active, -.btn:focus { - background: none; - border: none; +.button:hover, +.button:focus { + text-decoration: none; } -a.btn, -a.btn:hover { - color: inherit; -} - -/* By default, Fomantic UI doesn't support "bordered" buttons group, but Gitea would like to use it. -And the default buttons always have borders now (not the same as Fomantic UI's default buttons, see above). -It needs some tricks to tweak the left/right borders with active state */ - -.ui.buttons .button { - border-right: none; -} - -.ui.buttons .button:hover { - border-color: var(--color-secondary-dark-2); -} - -.ui.buttons .button:hover + .button { - border-left: 1px solid var(--color-secondary-dark-2); -} - -/* TODO: these "tw-hidden" selectors are only used by "blame.tmpl" buttons: Raw/Normal View/History/Unescape, need to be refactored to a clear solution later */ -.ui.buttons .button:first-child, -.ui.buttons .button.tw-hidden:first-child + .button { - border-left: 1px solid var(--color-light-border); -} - -.ui.buttons .button:last-child, -.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden) { - border-right: 1px solid var(--color-light-border); -} - -.ui.buttons .button.active { - border-left: 1px solid var(--color-light-border); - border-right: 1px solid var(--color-light-border); -} - -.ui.buttons .button.active + .button { - border-left: none; -} - -.ui.basic.buttons .button, -.ui.basic.button, -.ui.basic.buttons .button:hover, -.ui.basic.button:hover { - box-shadow: none; -} - -/* apply the vertical padding of .compact to non-compact buttons when they contain a svg as they - would otherwise appear too large. Seen on "RSS Feed" button on repo releases tab. */ -.ui.small.button:not(.compact):has(.svg) { - padding-top: 0.58928571em; - padding-bottom: 0.58928571em; -} - -.ui.labeled.button.disabled > .button, -.ui.basic.buttons .button, -.ui.basic.button { - color: var(--color-text-light); - background: var(--color-button); -} - -.ui.basic.buttons .button:hover, -.ui.basic.button:hover, -.ui.basic.buttons .button:focus, -.ui.basic.button:focus { - color: var(--color-text); - background: var(--color-hover); - border-color: var(--color-secondary-dark-2); -} - -.ui.basic.buttons .button:active, -.ui.basic.button:active, -.ui.basic.buttons .active.button, -.ui.basic.active.button, -.ui.basic.buttons .active.button:hover, -.ui.basic.active.button:hover, -.ui.basic.buttons .active.button:focus, -.ui.basic.active.button:focus { - color: var(--color-text); - background: var(--color-active); -} - -.ui.labeled.button > .label { - border-color: var(--color-light-border); -} - -.ui.labeled.icon.buttons > .button > .icon, -.ui.labeled.icon.button > .icon { - background: var(--color-hover); -} - -/* primary */ - -.ui.primary.labels .label, -.ui.ui.ui.primary.label, -.ui.primary.button, -.ui.primary.buttons .button { +.button.primary { background: var(--color-primary); color: var(--color-primary-contrast); } -.ui.primary.button:hover, -.ui.primary.buttons .button:hover, -.ui.primary.button:focus, -.ui.primary.buttons .button:focus { +.button.primary:hover, +.button.primary:focus { background: var(--color-primary-hover); - color: var(--color-primary-contrast); } -.ui.primary.button:active, -.ui.primary.buttons .button:active { - background: var(--color-primary-active); +.button.secondary { + background: var(--color-button); + border: 1px solid var(--color-light-border); + color: var(--color-text-light); } -.ui.basic.primary.buttons .button, -.ui.basic.primary.button { - color: var(--color-primary); - border-color: var(--color-primary); +.button.secondary:hover, +.button.secondary:focus { + color: var(--color-text); } -.ui.basic.primary.buttons .button:hover, -.ui.basic.primary.button:hover, -.ui.basic.primary.buttons .button:focus, -.ui.basic.primary.button:focus { - color: var(--color-primary-hover); - border-color: var(--color-primary-hover); +/* Dropdown openers should be at least tall as buttons they are in line with, and + * as wide as they are tall */ +.button-row details.dropdown summary, +.button-sequence details.dropdown summary { + min-height: var(--button-min-height); + min-width: var(--button-min-height); } -.ui.basic.primary.buttons .button:active, -.ui.basic.primary.button:active { - color: var(--color-primary-active); - border-color: var(--color-primary-active); +/* button-row is a simple helper made to improve consistency of fomantic buttons + * placed in a row. It provides gap and cancels out fomantic's margins */ +.button-row { + gap: var(--button-spacing); } -/* secondary */ - -.ui.secondary.labels .label, -.ui.ui.ui.secondary.label, -.ui.secondary.button, -.ui.secondary.buttons .button, -.ui.secondary.button:focus, -.ui.secondary.buttons .button:focus { - background: var(--color-secondary-button); -} - -.ui.secondary.button:hover, -.ui.secondary.buttons .button:hover { - background: var(--color-secondary-hover); -} - -.ui.secondary.button:active, -.ui.secondary.buttons .button:active { - background: var(--color-secondary-active); -} - -.ui.basic.secondary.buttons .button, -.ui.basic.secondary.button { - color: var(--color-secondary-button); - border-color: var(--color-secondary-button); -} - -.ui.basic.secondary.buttons .button:hover, -.ui.basic.secondary.button:hover, -.ui.basic.secondary.button:focus, -.ui.basic.secondary.buttons .button:focus { - color: var(--color-secondary-hover); - border-color: var(--color-secondary-hover); -} - -.ui.basic.secondary.buttons .button:active, -.ui.basic.secondary.button:active { - color: var(--color-secondary-active); - border-color: var(--color-secondary-active); -} - -/* red */ - -.ui.red.labels .label, -.ui.ui.ui.red.label, -.ui.red.button, -.ui.red.buttons .button { - background: var(--color-red); -} - -.ui.red.button:hover, -.ui.red.buttons .button:hover, -.ui.red.button:focus, -.ui.red.buttons .button:focus { - background: var(--color-red-dark-1); +/* button-sequence is a more complex helper that has wrap. Using it is preferred + * but also requires deeper consideration */ +.button-sequence { + display: flex; + flex-flow: wrap; + gap: var(--button-spacing); } -.ui.red.button:active, -.ui.red.buttons .button:active { - background: var(--color-red-dark-2); +/* Fomantic buttons have annoying margins to set distance between elements. In + * the button-row/sequence helpers this is set by flex+gap */ +.button-row .ui.button { + margin-right: 0; } - -.ui.basic.red.buttons .button, -.ui.basic.red.button { - color: var(--color-red); - border-color: var(--color-red); -} - -.ui.basic.red.buttons .button:hover, -.ui.basic.red.button:hover, -.ui.basic.red.buttons .button:focus, -.ui.basic.red.button:focus { - color: var(--color-red-dark-1); - border-color: var(--color-red-dark-1); -} - -.ui.basic.red.buttons .button:active, -.ui.basic.red.button:active { - color: var(--color-red-dark-2); - border-color: var(--color-red-dark-2); -} - -/* orange */ - -.ui.orange.labels .label, -.ui.ui.ui.orange.label, -.ui.orange.button, -.ui.orange.buttons .button, -.ui.orange.button:focus, -.ui.orange.buttons .button:focus { - background: var(--color-orange); -} - -.ui.orange.button:hover, -.ui.orange.buttons .button:hover { - background: var(--color-orange-dark-1); -} - -.ui.orange.button:active, -.ui.orange.buttons .button:active { - background: var(--color-orange-dark-2); -} - -.ui.basic.orange.buttons .button, -.ui.basic.orange.button, -.ui.basic.orange.buttons .button:focus, -.ui.basic.orange.button:focus { - color: var(--color-orange); - border-color: var(--color-orange); -} - -.ui.basic.orange.buttons .button:hover, -.ui.basic.orange.button:hover { - color: var(--color-orange-dark-1); - border-color: var(--color-orange-dark-1); -} - -.ui.basic.orange.buttons .button:active, -.ui.basic.orange.button:active { - color: var(--color-orange-dark-2); - border-color: var(--color-orange-dark-2); -} - -/* yellow */ - -.ui.yellow.labels .label, -.ui.ui.ui.yellow.label, -.ui.yellow.button, -.ui.yellow.buttons .button, -.ui.yellow.button:focus, -.ui.yellow.buttons .button:focus { - background: var(--color-yellow); -} - -.ui.yellow.button:hover, -.ui.yellow.buttons .button:hover { - background: var(--color-yellow-dark-1); -} - -.ui.yellow.button:active, -.ui.yellow.buttons .button:active { - background: var(--color-yellow-dark-2); -} - -.ui.basic.yellow.buttons .button, -.ui.basic.yellow.button, -.ui.basic.yellow.buttons .button:focus, -.ui.basic.yellow.button:focus { - color: var(--color-yellow); - border-color: var(--color-yellow); -} - -.ui.basic.yellow.buttons .button:hover, -.ui.basic.yellow.button:hover { - color: var(--color-yellow-dark-1); - border-color: var(--color-yellow-dark-1); -} - -.ui.basic.yellow.buttons .button:active, -.ui.basic.yellow.button:active { - color: var(--color-yellow-dark-2); - border-color: var(--color-yellow-dark-2); -} - -/* green */ - -.ui.green.labels .label, -.ui.ui.ui.green.label, -.ui.green.button, -.ui.green.buttons .button, -.ui.green.button:focus, -.ui.green.buttons .button:focus { - background: var(--color-green); -} - -.ui.green.button:hover, -.ui.green.buttons .button:hover { - background: var(--color-green-dark-1); -} - -.ui.green.button:active, -.ui.green.buttons .button:active { - background: var(--color-green-dark-2); -} - -.ui.basic.green.buttons .button, -.ui.basic.green.button, -.ui.basic.green.buttons .button:focus, -.ui.basic.green.button:focus { - color: var(--color-green); - border-color: var(--color-green); -} - -.ui.basic.green.buttons .button:hover, -.ui.basic.green.button:hover { - color: var(--color-green-dark-1); - border-color: var(--color-green-dark-1); -} - -.ui.basic.green.buttons .button:active, -.ui.basic.green.button:active { - color: var(--color-green-dark-2); - border-color: var(--color-green-dark-2); -} - -/* teal */ - -.ui.teal.labels .label, -.ui.ui.ui.teal.label, -.ui.teal.button, -.ui.teal.buttons .button, -.ui.teal.button:focus, -.ui.teal.buttons .button:focus { - background: var(--color-teal); -} - -.ui.teal.button:hover, -.ui.teal.buttons .button:hover { - background: var(--color-teal-dark-1); -} - -.ui.teal.button:active, -.ui.teal.buttons .button:active { - background: var(--color-teal-dark-2); -} - -.ui.basic.teal.buttons .button, -.ui.basic.teal.button, -.ui.basic.teal.buttons .button:focus, -.ui.basic.teal.button:focus { - color: var(--color-teal); - border-color: var(--color-teal); -} - -.ui.basic.teal.buttons .button:hover, -.ui.basic.teal.button:hover { - color: var(--color-teal-dark-1); - border-color: var(--color-teal-dark-1); -} - -.ui.basic.teal.buttons .button:active, -.ui.basic.teal.button:active { - color: var(--color-teal-dark-2); - border-color: var(--color-teal-dark-2); -} - -/* purple */ - -.ui.purple.labels .label, -.ui.ui.ui.purple.label, -.ui.purple.button, -.ui.purple.buttons .button, -.ui.purple.button:focus, -.ui.purple.buttons .button:focus { - background: var(--color-purple); -} - -.ui.purple.button:hover, -.ui.purple.buttons .button:hover { - background: var(--color-purple-dark-1); -} - -.ui.purple.button:active, -.ui.purple.buttons .button:active { - background: var(--color-purple-dark-2); -} - -.ui.basic.purple.buttons .button, -.ui.basic.purple.button, -.ui.basic.purple.buttons .button:focus, -.ui.basic.purple.button:focus { - color: var(--color-purple); - border-color: var(--color-purple); -} - -.ui.basic.purple.buttons .button:hover, -.ui.basic.purple.button:hover { - color: var(--color-purple-dark-1); - border-color: var(--color-purple-dark-1); -} - -.ui.basic.purple.buttons .button:active, -.ui.basic.purple.button:active { - color: var(--color-purple-dark-2); - border-color: var(--color-purple-dark-2); -} - -/* brown */ - -.ui.brown.labels .label, -.ui.ui.ui.brown.label, -.ui.brown.button, -.ui.brown.buttons .button, -.ui.brown.button:focus, -.ui.brown.buttons .button:focus { - background: var(--color-brown); -} - -.ui.brown.button:hover, -.ui.brown.buttons .button:hover { - background: var(--color-brown-dark-1); -} - -.ui.brown.button:active, -.ui.brown.buttons .button:active { - background: var(--color-brown-dark-2); -} - -.ui.basic.brown.buttons .button, -.ui.basic.brown.button, -.ui.basic.brown.buttons .button:focus, -.ui.basic.brown.button:focus { - color: var(--color-brown); - border-color: var(--color-brown); -} - -.ui.basic.brown.buttons .button:hover, -.ui.basic.brown.button:hover { - color: var(--color-brown-dark-1); - border-color: var(--color-brown-dark-1); -} - -.ui.basic.brown.buttons .button:active, -.ui.basic.brown.button:active { - color: var(--color-brown-dark-2); - border-color: var(--color-brown-dark-2); -} - -/* negative */ - -.ui.negative.buttons .button, -.ui.negative.button, -.ui.negative.buttons .button:focus, -.ui.negative.button:focus { - background: var(--color-red); -} - -.ui.negative.buttons .button:hover, -.ui.negative.button:hover { - background: var(--color-red-dark-1); -} - -.ui.negative.buttons .button:active, -.ui.negative.button:active { - background: var(--color-red-dark-2); -} - -.ui.basic.negative.buttons .button, -.ui.basic.negative.button, -.ui.basic.negative.buttons .button:focus, -.ui.basic.negative.button:focus { - color: var(--color-red); - border-color: var(--color-red); -} - -.ui.basic.negative.buttons .button:hover, -.ui.basic.negative.button:hover { - color: var(--color-red-dark-1); - border-color: var(--color-red-dark-1); -} - -.ui.basic.negative.buttons .button:active, -.ui.basic.negative.button:active { - color: var(--color-red-dark-2); - border-color: var(--color-red-dark-2); -} - -/* positive */ - -.ui.positive.buttons .button, -.ui.positive.button, -.ui.positive.buttons .button:focus, -.ui.positive.button:focus { - background: var(--color-green); -} - -.ui.positive.buttons .button:hover, -.ui.positive.button:hover { - background: var(--color-green-dark-1); -} - -.ui.positive.buttons .button:active, -.ui.positive.button:active { - background: var(--color-green-dark-2); -} - -.ui.basic.positive.buttons .button, -.ui.basic.positive.button, -.ui.basic.positive.buttons .button:focus, -.ui.basic.positive.button:focus { - color: var(--color-green); - border-color: var(--color-green); -} - -.ui.basic.positive.buttons .button:hover, -.ui.basic.positive.button:hover { - color: var(--color-green-dark-1); - border-color: var(--color-green-dark-1); -} - -.ui.basic.positive.buttons .button:active, -.ui.basic.positive.button:active { - color: var(--color-green-dark-2); - border-color: var(--color-green-dark-2); +.button-sequence .ui.button { + margin: 0 !important; } diff --git a/web_src/css/modules/dropdown.css b/web_src/css/modules/dropdown.css index 7695adfbaa..bdc703f3a6 100644 --- a/web_src/css/modules/dropdown.css +++ b/web_src/css/modules/dropdown.css @@ -1,3 +1,6 @@ +/* Copyright 2025 The Forgejo Authors. All rights reserved. + * SPDX-License-Identifier: GPL-3.0-or-later */ + /* This is an implementation of a dropdown menu based on details HTML tag. * It is inspired by https://picocss.com/docs/dropdown. * @@ -24,15 +27,22 @@ details.dropdown { details.dropdown > summary { /* Optional flex+gap in case summary contains multiple elements */ display: flex; - gap: 0.75rem; + gap: 0.25rem; align-items: center; - /* Cancel some of default styling */ - user-select: none; - list-style-type: none; + justify-content: center; /* Main visual properties */ - border: 1px solid var(--color-light-border); border-radius: var(--border-radius); padding: 0.5rem; + /* Unset unwanted default properties */ + user-select: none; + list-style-type: none; + + &.border { + border: 1px solid var(--color-light-border); + } + &.options { + padding-inline: 0.75rem; + } } details.dropdown > summary:hover, @@ -76,14 +86,33 @@ details.dropdown > summary + ul { details.dropdown > summary + ul > li { width: 100%; background: none; -} -details.dropdown > summary + ul > li:first-child { - border-radius: var(--border-radius) var(--border-radius) 0 0; -} + > :is(a, button) { + padding: var(--dropdown-item-padding); + width: 100%; + display: flex; + gap: 0.75rem; + align-items: center; + color: var(--color-text); + /* Suppress underline - hover is indicated by background color */ + text-decoration: none; -details.dropdown > summary + ul > li:last-child { - border-radius: 0 0 var(--border-radius) var(--border-radius); + &.active { + background: var(--color-active); + font-weight: var(--font-weight-medium); + } + } + /* Cancel default styling of button elements */ + > button { + background: none; + } + + &:first-child { + border-radius: var(--border-radius) var(--border-radius) 0 0; + } + &:last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); + } } /* dir-auto option - switch the direction at a width point where most of layout changes occur. */ @@ -92,9 +121,9 @@ details.dropdown > summary + ul > li:last-child { details.dropdown.dir-auto > summary + ul { inset-inline: 0 auto; direction: rtl; - } - details.dropdown.dir-auto > summary + ul > li { - direction: ltr; + > li { + direction: ltr; + } } } /* Note: https://css-tricks.com/css-anchor-positioning-guide/ @@ -103,23 +132,7 @@ details.dropdown > summary + ul > li:last-child { details.dropdown.dir-rtl > summary + ul { inset-inline: 0 auto; direction: rtl; -} -details.dropdown.dir-rtl > summary + ul > li { - direction: ltr; -} - -details.dropdown > summary + ul > li > .item { - padding: var(--dropdown-item-padding); - width: 100%; - display: flex; - gap: 0.75rem; - align-items: center; - color: var(--color-text); - /* Suppress underline - hover is indicated by background color */ - text-decoration: none; -} - -/* Cancel default styling of button elements */ -details.dropdown > summary + ul > li button { - background: none; + > li { + direction: ltr; + } } diff --git a/web_src/css/modules/stats-bar.css b/web_src/css/modules/stats-bar.css new file mode 100644 index 0000000000..73a66835d9 --- /dev/null +++ b/web_src/css/modules/stats-bar.css @@ -0,0 +1,130 @@ +/* Copyright 2025 The Forgejo Authors. All rights reserved. + * SPDX-License-Identifier: GPL-3.0-or-later */ + +:root stats-bar { + --stats-bar-height: 0.75rem; +} + +@media (pointer: coarse) { + :root stats-bar { + --stats-bar-height: 1rem; + } +} + +/* General properties, can be reused by different kinds of stats bars */ +stats-bar { + display: flex !important; + height: var(--stats-bar-height); + width: 100%; + border-radius: var(--border-radius); + + .slice { + display: inline-block; + + &:first-child { + border-radius: var(--border-radius) 0 0 var(--border-radius); + } + &:last-child { + border-radius: 0 var(--border-radius) var(--border-radius) 0; + } + &:only-child { + border-radius: var(--border-radius); + } + } +} + +/* Properties for stats bars with legend included via
      */ +details.stats { + /* Summary and legend should be stacked vertically */ + display: flex; + flex-direction: column; + + /* Prevent opener marker from showing up */ + summary { + display: flex; + } + + /* Cancel unwanted
        properties of legend */ + ul { + display: flex; + margin: 0; + } +} + +/* Properties specific to the UI in storage overview */ +.size-overview .stats { + border-radius: var(--border-radius); + + &:hover, + &[open] { + background-color: var(--color-box-body-highlight); + stats-bar { + background-color: var(--color-secondary-dark-3); + } + } + + &[open] { + gap: 0.25rem; + background-color: var(--color-box-body-highlight); + } + + summary { + flex-direction: column; + padding: 0.75rem; + gap: 1rem; + + stats-bar { + /* Same as default background of `progress` */ + background-color: var(--color-secondary-dark-1); + } + } + + ul { + flex-direction: column; + gap: 0.5rem; + padding: 0.75rem; + padding-top: 0; + + li { + display: flex; + align-items: center; + gap: 0.5rem; + } + } +} + +/* Properties specific to the lang stats in repo overview */ +#language-stats { + stats-bar { + gap: 2px; + border-radius: 0 0 var(--border-radius) var(--border-radius); + overflow: hidden; + .slice { + border-radius: 0; + } + } + &[open] stats-bar { + border-radius: 0; + } + + ul { + min-height: 2rem; + padding: 0 0.5rem; + flex-wrap: wrap; + justify-content: space-around; + gap: 0 1rem; + + li { + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem 0; + gap: 0.25em; + + span { + font-weight: var(--font-weight-semibold); + text-wrap: nowrap; + } + } + } +} diff --git a/web_src/css/modules/switch.css b/web_src/css/modules/switch.css index 6777c32fc8..e975c8991e 100644 --- a/web_src/css/modules/switch.css +++ b/web_src/css/modules/switch.css @@ -1,14 +1,15 @@ /* Copyright 2025 The Forgejo Authors. All rights reserved. -SPDX-License-Identifier: GPL-3.0-or-later */ + * SPDX-License-Identifier: GPL-3.0-or-later */ :root .switch { - --switch-padding-y: .5em; - --switch-padding-x: 1.125em; + /* The resulting switch height is --switch-item-min-height + 2px */ + --switch-item-min-height: 34px; + --switch-padding-inline: 1.125em; } @media (pointer: coarse) { :root .switch { - --switch-padding-y: .75em; + --switch-item-min-height: 38px; } } @@ -27,7 +28,8 @@ SPDX-License-Identifier: GPL-3.0-or-later */ display: flex; gap: 0.5rem; align-items: center; - padding: var(--switch-padding-y) var(--switch-padding-x); + padding-inline: var(--switch-padding-inline); + min-height: var(--switch-item-min-height); color: var(--color-text); border-radius: var(--border-radius); text-wrap: nowrap; @@ -42,16 +44,16 @@ SPDX-License-Identifier: GPL-3.0-or-later */ there are no ugly unpainted v/^ shapes between them */ .switch > .item:has(+ .active.item) { /* Active neighbor is next item */ margin-right: calc(-1 * var(--border-radius)); - padding-right: calc(var(--switch-padding-x) + var(--border-radius)); + padding-right: calc(var(--switch-padding-inline) + var(--border-radius)); } .switch > .active.item + .item { /* Active neighbor is previous item */ margin-left: calc(-1 * var(--border-radius)); - padding-left: calc(var(--switch-padding-x) + var(--border-radius)); + padding-left: calc(var(--switch-padding-inline) + var(--border-radius)); } .switch > .active.item { z-index: 2; - padding-left: var(--switch-padding-x); + padding-left: var(--switch-padding-inline); background: var(--color-active); outline: 1px solid var(--color-input-border); } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index f9e886e770..3d47ffece1 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -140,6 +140,13 @@ } } +@media (max-width: 390px) { + .repository .clone-panel #repo-clone-url { + width: 0; + flex-grow: 1; + } +} + .repository .ui.action.input.clone-panel > button + button, .repository .ui.action.input.clone-panel > button + input { margin-left: -1px; /* make the borders overlap to avoid double borders */ @@ -1890,25 +1897,6 @@ details.repo-search-result summary::marker { font-weight: var(--font-weight-medium); } -.repository .repository-summary #language-stats-bar { - display: flex; - gap: 2px; - padding: 0; - height: 10px; - white-space: nowrap; - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; - overflow: hidden; -} - -.size-overview .segment:has(> .bar) { - display: flex; - height: 10px; - padding: 0; - border-radius: 3px; - overflow: hidden; -} - #cite-repo-modal #citation-panel { display: flex; width: 100%; @@ -2132,6 +2120,10 @@ details.repo-search-result summary::marker { gap: 4px; } +.comment-header-left > .content-history-menu.active s { + text-decoration-thickness: 10%; +} + .comment-body { background: var(--color-box-body); border: none !important; @@ -2141,17 +2133,6 @@ details.repo-search-result summary::marker { padding: 1em; } -.edit-label.modal .form .column, -.new-label.modal .form .column { - padding-right: 0; -} - -.edit-label.modal .form .buttons, -.new-label.modal .form .buttons { - margin-left: auto; - padding-top: 15px; -} - .stats-table { display: table; width: 100%; diff --git a/web_src/css/repo/list-header.css b/web_src/css/repo/list-header.css index 304cfbc13c..4440bba8df 100644 --- a/web_src/css/repo/list-header.css +++ b/web_src/css/repo/list-header.css @@ -25,25 +25,6 @@ flex: 1; } -.small-menu-items { - min-height: 35.4px !important; /* match .small.button in height */ - background: none !important; /* fomantic sets a color here which does not play well with active transparent color on the item, so unset and set the colors on the item */ -} - -.small-menu-items .item { - background: var(--color-menu) !important; - padding-top: 6px !important; - padding-bottom: 6px !important; -} - -.small-menu-items .item:hover { - background: var(--color-hover) !important; -} - -.small-menu-items .item.active { - background: var(--color-active) !important; -} - @media (max-width: 767.98px) { .list-header-search { order: 0; diff --git a/web_src/css/themes/theme-forgejo-dark.css b/web_src/css/themes/theme-forgejo-dark.css index 94097497bf..66dfc65fa7 100644 --- a/web_src/css/themes/theme-forgejo-dark.css +++ b/web_src/css/themes/theme-forgejo-dark.css @@ -196,10 +196,15 @@ --color-orange-badge: #ea580c; --color-orange-badge-bg: #ea580c22; --color-orange-badge-hover-bg: #ea580c44; - /* Icon colors (PR/Issue/...) */ - --color-icon-green: #3fb950; - --color-icon-red: #f85149; - --color-icon-purple: #aa76ff; + + /* Colors for thin elements: octicons, text, borders */ + --color-chroma: 0.19; + --color-thin-lightness: 0.68; + --color-thin-green: oklch(var(--color-thin-lightness) var(--color-chroma) 145deg); + --color-thin-red: oklch(var(--color-thin-lightness) var(--color-chroma) 27deg); + --color-thin-purple: oklch(var(--color-thin-lightness) var(--color-chroma) 298deg); + --color-thin-orange: oklch(var(--color-thin-lightness) var(--color-chroma) 41deg); + /* target-based colors */ --color-body: var(--steel-800); --color-box-header: var(--steel-700); diff --git a/web_src/css/themes/theme-forgejo-light.css b/web_src/css/themes/theme-forgejo-light.css index 44b997b39c..b4ad8f1649 100644 --- a/web_src/css/themes/theme-forgejo-light.css +++ b/web_src/css/themes/theme-forgejo-light.css @@ -212,10 +212,6 @@ --color-orange-badge: #ea580c; --color-orange-badge-bg: #ea580c22; --color-orange-badge-hover-bg: #ea580c44; - /* Icon colors (PR/Issue/...) */ - --color-icon-green: var(--color-green-light); - --color-icon-red: var(--color-red-light); - --color-icon-purple: var(--color-purple-light); /* target-based colors */ --color-body: #fff; --color-box-header: var(--zinc-100); diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 84183a9e63..58e3a21f9a 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -179,10 +179,6 @@ --color-orange-badge: #f2711c; --color-orange-badge-bg: #f2711c1a; --color-orange-badge-hover-bg: #f2711c4d; - /* Icon colors (PR/Issue/...) */ - --color-icon-green: var(--color-green); - --color-icon-red: var(--color-red); - --color-icon-purple: var(--color-purple); /* target-based colors */ --color-body: #1c1f25; --color-box-header: #1a1d1f; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index aee47dc814..f889cac568 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -179,10 +179,6 @@ --color-orange-badge: #f2711c; --color-orange-badge-bg: #f2711c1a; --color-orange-badge-hover-bg: #f2711c4d; - /* Icon colors (PR/Issue/...) */ - --color-icon-green: var(--color-green); - --color-icon-red: var(--color-red); - --color-icon-purple: var(--color-purple); /* target-based colors */ --color-body: #ffffff; --color-box-header: #f1f3f5; diff --git a/web_src/fomantic/package-lock.json b/web_src/fomantic/package-lock.json index 38dd617a53..f428511af7 100644 --- a/web_src/fomantic/package-lock.json +++ b/web_src/fomantic/package-lock.json @@ -132,16 +132,16 @@ } }, "node_modules/@octokit/core": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.4.tgz", - "integrity": "sha512-jOT8V1Ba5BdC79sKrRWDdMT5l1R+XNHTPR6CPWzUP2EcfAcvIHZWF0eAbmRcpOOP5gVIwnqNg0C4nvh6Abc3OA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.5.tgz", + "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "license": "MIT", "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.1", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", + "@octokit/graphql": "^9.0.2", + "@octokit/request": "^10.0.4", + "@octokit/request-error": "^7.0.1", "@octokit/types": "^15.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" @@ -161,36 +161,19 @@ } }, "node_modules/@octokit/core/node_modules/@octokit/endpoint": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", - "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.1.tgz", + "integrity": "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^14.0.0", + "@octokit/types": "^15.0.0", "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/core/node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "license": "MIT", - "peer": true - }, - "node_modules/@octokit/core/node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz", @@ -199,15 +182,15 @@ "peer": true }, "node_modules/@octokit/core/node_modules/@octokit/request": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", - "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.5.tgz", + "integrity": "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/endpoint": "^11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", + "@octokit/endpoint": "^11.0.1", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" }, @@ -216,52 +199,18 @@ } }, "node_modules/@octokit/core/node_modules/@octokit/request-error": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", - "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.1.tgz", + "integrity": "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^14.0.0" + "@octokit/types": "^15.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/core/node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "license": "MIT", - "peer": true - }, - "node_modules/@octokit/core/node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/@octokit/core/node_modules/@octokit/request/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "license": "MIT", - "peer": true - }, - "node_modules/@octokit/core/node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, "node_modules/@octokit/core/node_modules/@octokit/types": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.0.tgz", @@ -304,14 +253,14 @@ "license": "ISC" }, "node_modules/@octokit/graphql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", - "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.2.tgz", + "integrity": "sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", + "@octokit/request": "^10.0.4", + "@octokit/types": "^15.0.0", "universal-user-agent": "^7.0.0" }, "engines": { @@ -319,13 +268,13 @@ } }, "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", - "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.1.tgz", + "integrity": "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^14.0.0", + "@octokit/types": "^15.0.0", "universal-user-agent": "^7.0.2" }, "engines": { @@ -333,22 +282,22 @@ } }, "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz", + "integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==", "license": "MIT", "peer": true }, "node_modules/@octokit/graphql/node_modules/@octokit/request": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", - "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.5.tgz", + "integrity": "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/endpoint": "^11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", + "@octokit/endpoint": "^11.0.1", + "@octokit/request-error": "^7.0.1", + "@octokit/types": "^15.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" }, @@ -357,26 +306,26 @@ } }, "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", - "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.1.tgz", + "integrity": "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/types": "^14.0.0" + "@octokit/types": "^15.0.0" }, "engines": { "node": ">= 20" } }, "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.0.tgz", + "integrity": "sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==", "license": "MIT", "peer": true, "dependencies": { - "@octokit/openapi-types": "^25.1.0" + "@octokit/openapi-types": "^26.0.0" } }, "node_modules/@octokit/graphql/node_modules/universal-user-agent": { @@ -545,12 +494,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "version": "24.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", + "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "license": "MIT", "dependencies": { - "undici-types": "~7.12.0" + "undici-types": "~7.14.0" } }, "node_modules/@types/vinyl": { @@ -1081,9 +1030,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", - "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -1177,9 +1126,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "funding": [ { "type": "opencollective", @@ -1196,9 +1145,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, @@ -1310,9 +1259,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001743", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", - "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", "funding": [ { "type": "opencollective", @@ -2054,9 +2003,9 @@ } }, "node_modules/editorconfig/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2066,9 +2015,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.222", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", - "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "version": "1.5.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", + "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -6038,9 +5987,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", "license": "MIT" }, "node_modules/node.extend": { @@ -8287,9 +8236,9 @@ } }, "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "license": "MIT" }, "node_modules/union-value": { diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index e8c1b76a7f..7debf6240e 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -1,5 +1,4 @@