{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
+ {{svg "octicon-triangle-down" 14 "dropdown icon"}}
{{ctx.Locale.Tr (printf "search.%s" .Selected)}}
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index bd2a380..85ae726 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -103,11 +103,11 @@
{{ctx.Locale.Tr "action.compare_commits" $push.Len}} »
{{end}}
{{else if .GetOpType.InActions "create_issue"}}
-
{{index .GetIssueInfos 1 | RenderEmoji $.Context | RenderCodeBlock}}
+
{{RenderIssueTitle ctx (index .GetIssueInfos 1) (.Repo.ComposeMetas ctx)}}
{{else if .GetOpType.InActions "create_pull_request"}}
-
{{index .GetIssueInfos 1 | RenderEmoji $.Context | RenderCodeBlock}}
+
{{RenderIssueTitle ctx (index .GetIssueInfos 1) (.Repo.ComposeMetas ctx)}}
{{else if .GetOpType.InActions "comment_issue" "approve_pull_request" "reject_pull_request" "comment_pull"}}
-
{{(.GetIssueTitle ctx) | RenderEmoji $.Context | RenderCodeBlock}}
+
{{RenderIssueTitle ctx (.GetIssueTitle ctx) (.Repo.ComposeMetas ctx)}}
{{$comment := index .GetIssueInfos 1}}
{{if $comment}}
{{RenderMarkdownToHtml ctx $comment}}
@@ -115,7 +115,7 @@
{{else if .GetOpType.InActions "merge_pull_request"}}
{{index .GetIssueInfos 1}}
{{else if .GetOpType.InActions "close_issue" "reopen_issue" "close_pull_request" "reopen_pull_request"}}
-
{{(.GetIssueTitle ctx) | RenderEmoji $.Context | RenderCodeBlock}}
+
{{RenderIssueTitle ctx (.GetIssueTitle ctx) (.Repo.ComposeMetas ctx)}}
{{else if .GetOpType.InActions "pull_review_dismissed"}}
{{ctx.Locale.Tr "action.review_dismissed_reason"}}
{{index .GetIssueInfos 2 | RenderEmoji $.Context}}
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index 8d8858b..35fc5e7 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -250,16 +250,18 @@ test('For anyone', async ({page}) => {
If you need a user account, you can use something like:
~~~js
-import {test, login_user, login} from './utils_e2e.ts';
+import {test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2'); // or another user
-});
+// reuse user2 token from scope `shared`
+test.use({user: 'user2', authScope: 'shared'})
-test('For signed users only', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('For signed users only', async ({page}) => {
+
+})
~~~
+users are created in [utils_e2e_test.go](utils_e2e_test.go)
+
### Run tests very selectively
Browser testing can take some time.
diff --git a/tests/e2e/actions.test.e2e.ts b/tests/e2e/actions.test.e2e.ts
index a66b608..4e93b89 100644
--- a/tests/e2e/actions.test.e2e.ts
+++ b/tests/e2e/actions.test.e2e.ts
@@ -10,76 +10,66 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
-
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+import {save_visual, test} from './utils_e2e.ts';
const workflow_trigger_notification_text = 'This workflow has a workflow_dispatch event trigger.';
+test.describe('Workflow Authenticated user2', () => {
+ test.use({user: 'user2'});
-test('workflow dispatch present', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+ test('workflow dispatch present', async ({page}) => {
+ await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
- await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
+ await expect(page.getByText(workflow_trigger_notification_text)).toBeVisible();
- await expect(page.getByText(workflow_trigger_notification_text)).toBeVisible();
+ const run_workflow_btn = page.locator('#workflow_dispatch_dropdown>button');
+ await expect(run_workflow_btn).toBeVisible();
- const run_workflow_btn = page.locator('#workflow_dispatch_dropdown>button');
- await expect(run_workflow_btn).toBeVisible();
-
- const menu = page.locator('#workflow_dispatch_dropdown>.menu');
- await expect(menu).toBeHidden();
- await run_workflow_btn.click();
- await expect(menu).toBeVisible();
- await save_visual(page);
-});
-
-test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) => {
- test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
-
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
-
- await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
-
- await page.locator('#workflow_dispatch_dropdown>button').click();
-
- // Remove the required attribute so we can trigger the error message!
- await page.evaluate(() => {
- const elem = document.querySelector('input[name="inputs[string2]"]');
- elem?.removeAttribute('required');
+ const menu = page.locator('#workflow_dispatch_dropdown>.menu');
+ await expect(menu).toBeHidden();
+ await run_workflow_btn.click();
+ await expect(menu).toBeVisible();
+ await save_visual(page);
});
- await page.locator('#workflow-dispatch-submit').click();
+ test('dispatch error: missing inputs', async ({page}, testInfo) => {
+ test.skip(testInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
- await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible();
- await save_visual(page);
-});
+ await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
-test('workflow dispatch success', async ({browser}, workerInfo) => {
- test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
+ await page.locator('#workflow_dispatch_dropdown>button').click();
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+ // Remove the required attribute so we can trigger the error message!
+ await page.evaluate(() => {
+ const elem = document.querySelector('input[name="inputs[string2]"]');
+ elem?.removeAttribute('required');
+ });
- await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
+ await page.locator('#workflow-dispatch-submit').click();
- await page.locator('#workflow_dispatch_dropdown>button').click();
+ await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible();
+ await save_visual(page);
+ });
- await page.fill('input[name="inputs[string2]"]', 'abc');
- await save_visual(page);
- await page.locator('#workflow-dispatch-submit').click();
+ test('dispatch success', async ({page}, testInfo) => {
+ test.skip(testInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
+ await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
- await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible();
+ await page.locator('#workflow_dispatch_dropdown>button').click();
- await expect(page.locator('.run-list>:first-child .run-list-meta', {hasText: 'now'})).toBeVisible();
- await save_visual(page);
+ await page.fill('input[name="inputs[string2]"]', 'abc');
+ await save_visual(page);
+ 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);
+ });
});
test('workflow dispatch box not available for unauthenticated users', async ({page}) => {
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);
});
diff --git a/tests/e2e/clipboard-copy.test.e2e.ts b/tests/e2e/clipboard-copy.test.e2e.ts
index 70a3425..2ae0e0d 100644
--- a/tests/e2e/clipboard-copy.test.e2e.ts
+++ b/tests/e2e/clipboard-copy.test.e2e.ts
@@ -8,7 +8,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.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!');
@@ -19,6 +19,7 @@ test('copy src file path to clipboard', async ({page}, workerInfo) => {
await page.click('[data-clipboard-text]');
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
+ await save_visual(page);
});
test('copy diff file path to clipboard', async ({page}, workerInfo) => {
@@ -30,4 +31,6 @@ test('copy diff file path to clipboard', async ({page}, workerInfo) => {
await page.click('[data-clipboard-text]');
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
+ await expect(page.getByText('Copied')).toBeVisible();
+ await save_visual(page);
});
diff --git a/tests/e2e/dashboard-ci-status.test.e2e.ts b/tests/e2e/dashboard-ci-status.test.e2e.ts
index 1d23122..d35fe29 100644
--- a/tests/e2e/dashboard-ci-status.test.e2e.ts
+++ b/tests/e2e/dashboard-ci-status.test.e2e.ts
@@ -3,22 +3,26 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
+import {test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('Correct link and tooltip', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+test.describe.configure({retries: 2});
+
+test('Correct link and tooltip', async ({page}, testInfo) => {
+ if (testInfo.retry) {
+ await page.goto('/user2/test_workflows/actions');
+ }
+
+ const searchResponse = page.waitForResponse((resp) => resp.url().includes('/repo/search?') && resp.status() === 200);
const response = await page.goto('/?repo-search-query=test_workflows');
expect(response?.status()).toBe(200);
+ await searchResponse;
+
const repoStatus = page.locator('.dashboard-repos .repo-owner-name-list > li:nth-child(1) > a:nth-child(2)');
- // wait for network activity to cease (so status was loaded in frontend)
- await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
await expect(repoStatus).toHaveAttribute('href', '/user2/test_workflows/actions', {timeout: 10000});
await expect(repoStatus).toHaveAttribute('data-tooltip-content', /^(Error|Failure)$/);
- await save_visual(page);
+ // ToDo: Ensure stable screenshot of dashboard. Known to be flaky: https://code.forgejo.org/forgejo/visual-browser-testing/commit/206d4cfb7a4af6d8d7043026cdd4d63708798b2a
+ // await save_visual(page);
});
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index b8c8962..245bd34 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -89,6 +89,7 @@ func TestE2e(t *testing.T) {
runArgs := []string{"npx", "playwright", "test"}
+ _, testVisual := os.LookupEnv("VISUAL_TEST")
// To update snapshot outputs
if _, set := os.LookupEnv("ACCEPT_VISUAL"); set {
runArgs = append(runArgs, "--update-snapshots")
@@ -112,6 +113,10 @@ func TestE2e(t *testing.T) {
onForgejoRun(t, func(*testing.T, *url.URL) {
defer DeclareGitRepos(t)()
thisTest := runArgs
+ // when all tests are run, use unique artifacts directories per test to preserve artifacts from other tests
+ if testVisual {
+ thisTest = append(thisTest, "--output=tests/e2e/test-artifacts/"+testname)
+ }
thisTest = append(thisTest, path)
cmd := exec.Command(runArgs[0], thisTest...)
cmd.Env = os.Environ()
@@ -121,7 +126,7 @@ func TestE2e(t *testing.T) {
cmd.Stderr = os.Stderr
err := cmd.Run()
- if err != nil {
+ if err != nil && !testVisual {
log.Fatal("Playwright Failed: %s", err)
}
})
diff --git a/tests/e2e/example.test.e2e.ts b/tests/e2e/example.test.e2e.ts
index b2a679a..97c5b86 100644
--- a/tests/e2e/example.test.e2e.ts
+++ b/tests/e2e/example.test.e2e.ts
@@ -5,7 +5,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
test('Load Homepage', async ({page}) => {
const response = await page.goto('/');
@@ -26,6 +26,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);
});
// 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 44c9b21..1bb5af3 100644
--- a/tests/e2e/explore.test.e2e.ts
+++ b/tests/e2e/explore.test.e2e.ts
@@ -7,7 +7,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
test('Explore view taborder', async ({page}) => {
await page.goto('/explore/repos');
@@ -42,4 +42,5 @@ test('Explore view taborder', async ({page}) => {
}
}
expect(res).toBe(exp);
+ await save_visual(page);
});
diff --git a/tests/e2e/git-notes.test.e2e.ts b/tests/e2e/git-notes.test.e2e.ts
index 8b80a3a..1e2cbe7 100644
--- a/tests/e2e/git-notes.test.e2e.ts
+++ b/tests/e2e/git-notes.test.e2e.ts
@@ -1,17 +1,16 @@
// @ts-check
-import {test, expect} from '@playwright/test';
-import {login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
+import {expect} from '@playwright/test';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('Change git note', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+test('Change git note', async ({page}) => {
let response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
expect(response?.status()).toBe(200);
+ // An add button should not be present, because the commit already has a commit note
+ await expect(page.locator('#commit-notes-add-button')).toHaveCount(0);
+
await page.locator('#commit-notes-edit-button').click();
let textarea = page.locator('textarea[name="notes"]');
diff --git a/tests/e2e/issue-comment.test.e2e.ts b/tests/e2e/issue-comment.test.e2e.ts
index 4fce167..1c19f98 100644
--- a/tests/e2e/issue-comment.test.e2e.ts
+++ b/tests/e2e/issue-comment.test.e2e.ts
@@ -5,14 +5,11 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {test, save_visual} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('Menu accessibility', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('Menu accessibility', async ({page}) => {
await page.goto('/user2/repo1/issues/1');
await expect(page.getByLabel('user2 reacted eyes. Remove eyes')).toBeVisible();
await expect(page.getByLabel('reacted laugh. Remove laugh')).toBeVisible();
@@ -24,9 +21,8 @@ test('Menu accessibility', async ({browser}, workerInfo) => {
await expect(page.getByLabel('user1, user2 reacted laugh. Remove laugh')).toBeVisible();
});
-test('Hyperlink paste behaviour', async ({browser}, workerInfo) => {
+test('Hyperlink paste behaviour', async ({page}, workerInfo) => {
test.skip(['Mobile Safari', 'Mobile Chrome', 'webkit'].includes(workerInfo.project.name), 'Mobile clients seem to have very weird behaviour with this test, which I cannot confirm with real usage');
- const page = await login({browser}, workerInfo);
await page.goto('/user2/repo1/issues/new');
await page.locator('textarea').click();
// same URL
@@ -58,8 +54,7 @@ test('Hyperlink paste behaviour', async ({browser}, workerInfo) => {
await page.locator('textarea').fill('');
});
-test('Always focus edit tab first on edit', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('Always focus edit tab first on edit', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
@@ -82,9 +77,29 @@ test('Always focus edit tab first on edit', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('Quote reply', async ({browser}, workerInfo) => {
+test('Reset content of comment edit field on cancel', async ({page}) => {
+ const response = await page.goto('/user2/repo1/issues/1');
+ expect(response?.status()).toBe(200);
+
+ const editorTextarea = page.locator('[id="_combo_markdown_editor_1"]');
+
+ // Change the content of the edit field
+ 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 editorTextarea.fill('some random string');
+ await expect(editorTextarea).toHaveValue('some random string');
+ await page.click('#issue-1 .comment-container .edit .cancel');
+
+ // Edit again and assert that the edit field should be reset to the initial content
+ 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);
+});
+
+test('Quote reply', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
@@ -157,9 +172,8 @@ test('Quote reply', async ({browser}, workerInfo) => {
await editorTextarea.fill('');
});
-test('Pull quote reply', async ({browser}, workerInfo) => {
+test('Pull quote reply', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/commitsonpr/pulls/1/files');
expect(response?.status()).toBe(200);
diff --git a/tests/e2e/issue-sidebar.test.e2e.ts b/tests/e2e/issue-sidebar.test.e2e.ts
index f4d50a1..fe2a6ce 100644
--- a/tests/e2e/issue-sidebar.test.e2e.ts
+++ b/tests/e2e/issue-sidebar.test.e2e.ts
@@ -7,14 +7,13 @@
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */
import {expect, type Page} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
test.describe('Pull: Toggle WIP', () => {
const prTitle = 'pull5';
+
async function toggle_wip_to({page}, should: boolean) {
await page.waitForLoadState('domcontentloaded');
if (should) {
@@ -39,8 +38,7 @@ test.describe('Pull: Toggle WIP', () => {
}
}
- test.beforeEach(async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+ test.beforeEach(async ({page}) => {
const response = await page.goto('/user2/repo1/pulls/5');
expect(response?.status()).toBe(200); // Status OK
// ensure original title
@@ -50,9 +48,8 @@ test.describe('Pull: Toggle WIP', () => {
await check_wip({page}, false);
});
- test('simple toggle', async ({browser}, workerInfo) => {
+ test('simple toggle', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
await page.goto('/user2/repo1/pulls/5');
// toggle to WIP
await toggle_wip_to({page}, true);
@@ -62,9 +59,8 @@ test.describe('Pull: Toggle WIP', () => {
await check_wip({page}, false);
});
- test('manual edit', async ({browser}, workerInfo) => {
+ test('manual edit', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
await page.goto('/user2/repo1/pulls/5');
// manually edit title to another prefix
await page.locator('#issue-title-edit-show').click();
@@ -76,9 +72,8 @@ test.describe('Pull: Toggle WIP', () => {
await check_wip({page}, false);
});
- test('maximum title length', async ({browser}, workerInfo) => {
+ test('maximum title length', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
await page.goto('/user2/repo1/pulls/5');
// check maximum title length is handled gracefully
const maxLenStr = prTitle + 'a'.repeat(240);
@@ -96,17 +91,16 @@ test.describe('Pull: Toggle WIP', () => {
});
});
-test('Issue: Labels', async ({browser}, workerInfo) => {
+test('Issue: Labels', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- async function submitLabels({page}: {page: Page}) {
+ async function submitLabels({page}: { page: Page }) {
const submitted = page.waitForResponse('/user2/repo1/issues/labels');
await page.locator('textarea').first().click(); // close via unrelated element
await submitted;
await page.waitForLoadState();
}
- const page = await login({browser}, workerInfo);
// select label list in sidebar only
const labelList = page.locator('.issue-content-right .labels-list a');
const response = await page.goto('/user2/repo1/issues/1');
@@ -144,9 +138,8 @@ test('Issue: Labels', async ({browser}, workerInfo) => {
await expect(labelList.filter({hasText: 'label1'})).toBeVisible();
});
-test('Issue: Assignees', async ({browser}, workerInfo) => {
+test('Issue: Assignees', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
// select label list in sidebar only
const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item a');
@@ -182,9 +175,8 @@ test('Issue: Assignees', async ({browser}, workerInfo) => {
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeHidden();
});
-test('New Issue: Assignees', async ({browser}, workerInfo) => {
+test('New Issue: Assignees', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
// select label list in sidebar only
const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item');
@@ -224,9 +216,8 @@ test('New Issue: Assignees', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('Issue: Milestone', async ({browser}, workerInfo) => {
+test('Issue: Milestone', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
@@ -248,9 +239,8 @@ test('Issue: Milestone', async ({browser}, workerInfo) => {
await expect(page.locator('.timeline-item.event').last()).toContainText('user2 removed this from the milestone1 milestone');
});
-test('New Issue: Milestone', async ({browser}, workerInfo) => {
+test('New Issue: Milestone', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts
index ca2d6e0..35e9de2 100644
--- a/tests/e2e/markdown-editor.test.e2e.ts
+++ b/tests/e2e/markdown-editor.test.e2e.ts
@@ -5,21 +5,16 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, load_logged_in_context, login_user} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('Markdown image preview behaviour', async ({browser}, workerInfo) => {
+test('Markdown image preview behaviour', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari;');
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
-
// Editing the root README.md file for image preview
const editPath = '/user2/repo1/src/branch/master/README.md';
- const page = await context.newPage();
const response = await page.goto(editPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
@@ -43,12 +38,9 @@ test('Markdown image preview behaviour', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('markdown indentation', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
-
+test('markdown indentation', async ({page}) => {
const initText = `* first\n* second\n* third\n* last`;
- const page = await context.newPage();
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
@@ -116,12 +108,9 @@ test('markdown indentation', async ({browser}, workerInfo) => {
await expect(textarea).toHaveValue(initText);
});
-test('markdown list continuation', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
+test('markdown list continuation', async ({page}) => {
+ const initText = `* first\n* second`;
- const initText = `* first\n* second\n* third\n* last`;
-
- const page = await context.newPage();
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
@@ -130,25 +119,20 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
const indent = page.locator('button[data-md-action="indent"]');
await textarea.fill(initText);
- // Test continuation of '* ' prefix
- await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
+ // Test continuation of ' * ' prefix
+ await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('rst'), it.value.indexOf('rst')));
+ await indent.click();
await textarea.press('End');
await textarea.press('Enter');
- await textarea.pressSequentially('middle');
- await expect(textarea).toHaveValue(`* first\n* second\n* middle\n* third\n* last`);
-
- // Test continuation of ' * ' prefix
- await indent.click();
- await textarea.press('Enter');
await textarea.pressSequentially('muddle');
- await expect(textarea).toHaveValue(`* first\n* second\n${tab}* middle\n${tab}* muddle\n* third\n* last`);
+ await expect(textarea).toHaveValue(`${tab}* first\n${tab}* muddle\n* second`);
// Test breaking in the middle of a line
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.lastIndexOf('ddle'), it.value.lastIndexOf('ddle')));
await textarea.pressSequentially('tate');
await textarea.press('Enter');
await textarea.pressSequentially('me');
- await expect(textarea).toHaveValue(`* first\n* second\n${tab}* middle\n${tab}* mutate\n${tab}* meddle\n* third\n* last`);
+ await expect(textarea).toHaveValue(`${tab}* first\n${tab}* mutate\n${tab}* meddle\n* second`);
// Test not triggering when Shift held
await textarea.fill(initText);
@@ -156,35 +140,36 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
await textarea.press('Shift+Enter');
await textarea.press('Enter');
await textarea.pressSequentially('...but not least');
- await expect(textarea).toHaveValue(`* first\n* second\n* third\n* last\n\n...but not least`);
+ await expect(textarea).toHaveValue(`* first\n* second\n\n...but not least`);
// Test continuation of ordered list
- await textarea.fill(`1. one\n2. two`);
+ await textarea.fill(`1. one`);
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
+ await textarea.pressSequentially(' ');
+ await textarea.press('Enter');
await textarea.pressSequentially('three');
- await expect(textarea).toHaveValue(`1. one\n2. two\n3. three`);
+ await textarea.press('Enter');
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`1. one\n2. \n3. three\n\n`);
// Test continuation of alternative ordered list syntax
- await textarea.fill(`1) one\n2) two`);
+ await textarea.fill(`1) one`);
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
+ await textarea.pressSequentially(' ');
+ await textarea.press('Enter');
await textarea.pressSequentially('three');
- await expect(textarea).toHaveValue(`1) one\n2) two\n3) three`);
-
- // Test continuation of blockquote
- await textarea.fill(`> knowledge is power`);
- await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
- await textarea.pressSequentially('france is bacon');
- await expect(textarea).toHaveValue(`> knowledge is power\n> france is bacon`);
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`1) one\n2) \n3) three\n\n`);
// Test continuation of checklists
- await textarea.fill(`- [ ] have a problem\n- [x] create a solution`);
+ await textarea.fill(`- [ ]have a problem\n- [x]create a solution`);
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
await textarea.pressSequentially('write a test');
- await expect(textarea).toHaveValue(`- [ ] have a problem\n- [x] create a solution\n- [ ] write a test`);
+ await expect(textarea).toHaveValue(`- [ ]have a problem\n- [x]create a solution\n- [ ]write a test`);
// Test all conceivable syntax (except ordered lists)
const prefixes = [
@@ -200,7 +185,6 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
'> ',
'> > ',
'- [ ] ',
- '- [ ]', // This does seem to render, so allow.
'* [ ] ',
'+ [ ] ',
];
@@ -208,15 +192,16 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
await textarea.fill(`${prefix}one`);
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
+ await textarea.pressSequentially(' ');
+ await textarea.press('Enter');
await textarea.pressSequentially('two');
- await expect(textarea).toHaveValue(`${prefix}one\n${prefix}two`);
+ await textarea.press('Enter');
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`${prefix}one\n${prefix} \n${prefix}two\n\n`);
}
});
-test('markdown insert table', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
-
- const page = await context.newPage();
+test('markdown insert table', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
@@ -238,3 +223,29 @@ test('markdown insert table', async ({browser}, workerInfo) => {
await expect(textarea).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
await save_visual(page);
});
+
+test('text expander has higher prio then prefix continuation', async ({page}) => {
+ const response = await page.goto('/user2/repo1/issues/new');
+ expect(response?.status()).toBe(200);
+
+ const textarea = page.locator('textarea[name=content]');
+ const initText = `* first`;
+ await textarea.fill(initText);
+ await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('rst'), it.value.indexOf('rst')));
+ await textarea.press('End');
+
+ // Test emoji completion
+ await textarea.press('Enter');
+ await textarea.pressSequentially(':smile_c');
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`* first\n* 😸`);
+
+ // Test username completion
+ await textarea.press('Enter');
+ await textarea.pressSequentially('@user');
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 `);
+
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* `);
+});
diff --git a/tests/e2e/markup.test.e2e.ts b/tests/e2e/markup.test.e2e.ts
index 2726942..398a0a6 100644
--- a/tests/e2e/markup.test.e2e.ts
+++ b/tests/e2e/markup.test.e2e.ts
@@ -3,7 +3,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
test('markup with #xyz-mode-only', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
@@ -13,4 +13,5 @@ test('markup with #xyz-mode-only', async ({page}) => {
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);
});
diff --git a/tests/e2e/org-settings.test.e2e.ts b/tests/e2e/org-settings.test.e2e.ts
index 22a8bc0..df554e0 100644
--- a/tests/e2e/org-settings.test.e2e.ts
+++ b/tests/e2e/org-settings.test.e2e.ts
@@ -5,16 +5,13 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
import {validate_form} from './shared/forms.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('org team settings', async ({browser}, workerInfo) => {
+test('org team settings', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/org/org3/teams/team1/edit');
expect(response?.status()).toBe(200);
diff --git a/tests/e2e/profile_actions.test.e2e.ts b/tests/e2e/profile_actions.test.e2e.ts
index 65090e6..a66dc43 100644
--- a/tests/e2e/profile_actions.test.e2e.ts
+++ b/tests/e2e/profile_actions.test.e2e.ts
@@ -5,13 +5,11 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test('Follow actions', async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+test.use({user: 'user2'});
+test('Follow actions', async ({page}) => {
await page.goto('/user1');
// Check if following and then unfollowing works.
diff --git a/tests/e2e/reaction-selectors.test.e2e.ts b/tests/e2e/reaction-selectors.test.e2e.ts
index 3ce71b2..54b7d91 100644
--- a/tests/e2e/reaction-selectors.test.e2e.ts
+++ b/tests/e2e/reaction-selectors.test.e2e.ts
@@ -4,11 +4,9 @@
// @watch end
import {expect, type Locator} from '@playwright/test';
-import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
const assertReactionCounts = (comment: Locator, counts: unknown) =>
expect(async () => {
@@ -26,6 +24,7 @@ const assertReactionCounts = (comment: Locator, counts: unknown) =>
]),
),
);
+ // eslint-disable-next-line playwright/no-standalone-expect
return expect(reactions).toStrictEqual(counts);
}).toPass();
@@ -35,10 +34,7 @@ async function toggleReaction(menu: Locator, reaction: string) {
await menu.locator(`[role=menuitem][data-reaction-content="${reaction}"]`).click();
}
-test('Reaction Selectors', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
-
+test('Reaction Selectors', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
diff --git a/tests/e2e/release.test.e2e.ts b/tests/e2e/release.test.e2e.ts
index fefa446..49c6779 100644
--- a/tests/e2e/release.test.e2e.ts
+++ b/tests/e2e/release.test.e2e.ts
@@ -9,24 +9,18 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
import {validate_form} from './shared/forms.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
test.describe.configure({
timeout: 30000,
});
-test('External Release Attachments', async ({browser, isMobile}, workerInfo) => {
+test('External Release Attachments', async ({page, isMobile}) => {
test.skip(isMobile);
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- /** @type {import('@playwright/test').Page} */
- const page = await context.newPage();
-
// Click "New Release"
await page.goto('/user2/repo2/releases');
await page.click('.button.small.primary');
diff --git a/tests/e2e/repo-code.test.e2e.ts b/tests/e2e/repo-code.test.e2e.ts
index 264dd3a..11b710c 100644
--- a/tests/e2e/repo-code.test.e2e.ts
+++ b/tests/e2e/repo-code.test.e2e.ts
@@ -5,13 +5,9 @@
// @watch end
import {expect, type Page} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
import {accessibilityCheck} from './shared/accessibility.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
-
async function assertSelectedLines(page: Page, nums: string[]) {
const pageAssertions = async () => {
expect(
@@ -53,6 +49,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);
});
test('Readable diff', async ({page}, workerInfo) => {
@@ -79,22 +76,26 @@ 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);
});
-test('Username highlighted in commits', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
- await page.goto('/user2/mentions-highlighted/commits/branch/main');
- // check first commit
- await page.getByRole('link', {name: 'A commit message which'}).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);
- // 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);
+test.describe('As authenticated user', () => {
+ test.use({user: 'user2'});
+
+ test('Username highlighted in commits', async ({page}) => {
+ await page.goto('/user2/mentions-highlighted/commits/branch/main');
+ // check first commit
+ await page.getByRole('link', {name: 'A commit message which'}).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);
+ // 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);
+ });
});
diff --git a/tests/e2e/repo-commitgraph.test.e2e.ts b/tests/e2e/repo-commitgraph.test.e2e.ts
index 5f0cad1..e8b85c5 100644
--- a/tests/e2e/repo-commitgraph.test.e2e.ts
+++ b/tests/e2e/repo-commitgraph.test.e2e.ts
@@ -5,13 +5,30 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
test('Commit graph overflow', async ({page}) => {
- await page.goto('/user2/diff-test/graph');
+ const response = await page.goto('/user2/repo1/graph');
+ expect(response?.status()).toBe(200);
+
+ await page.click('#flow-select-refs-dropdown');
+ const input = page.locator('#flow-select-refs-dropdown');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+
+ await expect(page.locator('#flow-select-refs-dropdown')).toBeInViewport({ratio: 1});
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);
});
test('Switch branch', async ({page}) => {
@@ -28,4 +45,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);
});
diff --git a/tests/e2e/repo-migrate.test.e2e.ts b/tests/e2e/repo-migrate.test.e2e.ts
index a0f9ab6..5e67f89 100644
--- a/tests/e2e/repo-migrate.test.e2e.ts
+++ b/tests/e2e/repo-migrate.test.e2e.ts
@@ -3,15 +3,13 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts';
+import {test, save_visual, test_context} from './utils_e2e.ts';
-test.beforeAll(({browser}, workerInfo) => login_user(browser, workerInfo, 'user2'));
+test.use({user: 'user2'});
-test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo) => {
+test('Migration Progress Page', async ({page, browser}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky actionability checks on Mobile Safari');
- const page = await (await load_logged_in_context(browser, workerInfo, 'user2')).newPage();
-
expect((await page.goto('/user2/invalidrepo'))?.status(), 'repo should not exist yet').toBe(404);
await page.goto('/repo/migrate?service_type=1');
@@ -23,10 +21,11 @@ test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo
await form.locator('button.primary').click({timeout: 5000});
await expect(page).toHaveURL('user2/invalidrepo');
await save_visual(page);
- // page screenshot of unauthedPage is checked automatically after the test
- expect((await unauthedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200);
- await expect(unauthedPage.locator('#repo_migrating_progress')).toBeVisible();
+ const ctx = await test_context(browser);
+ const unauthenticatedPage = await ctx.newPage();
+ expect((await unauthenticatedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200);
+ await expect(unauthenticatedPage.locator('#repo_migrating_progress')).toBeVisible();
await page.reload();
await expect(page.locator('#repo_migrating_failed')).toBeVisible();
@@ -37,4 +36,6 @@ test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo
await save_visual(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);
});
diff --git a/tests/e2e/repo-new.test.e2e.ts b/tests/e2e/repo-new.test.e2e.ts
index c9cc29a..ad20282 100644
--- a/tests/e2e/repo-new.test.e2e.ts
+++ b/tests/e2e/repo-new.test.e2e.ts
@@ -4,15 +4,12 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, dynamic_id, save_visual, login_user, login} from './utils_e2e.ts';
+import {test, dynamic_id, save_visual} from './utils_e2e.ts';
import {validate_form} from './shared/forms.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('New repo: invalid', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('New repo: invalid', async ({page}) => {
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
// check that relevant form content is hidden or available
@@ -28,8 +25,7 @@ test('New repo: invalid', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('New repo: initialize', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('New repo: initialize', async ({page}, workerInfo) => {
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
// check that relevant form content is hidden or available
@@ -62,8 +58,7 @@ test('New repo: initialize', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('New repo: initialize later', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('New repo: initialize later', async ({page}) => {
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
@@ -97,9 +92,8 @@ test('New repo: initialize later', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('New repo: from template', async ({browser}, workerInfo) => {
+test('New repo: from template', async ({page}, workerInfo) => {
test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'WebKit browsers seem to have CORS issues with localhost here.');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
@@ -114,8 +108,7 @@ test('New repo: from template', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('New repo: label set', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('New repo: label set', async ({page}) => {
await page.goto('/repo/create');
const reponame = dynamic_id();
diff --git a/tests/e2e/repo-settings.test.e2e.ts b/tests/e2e/repo-settings.test.e2e.ts
index 113b151..3d26086 100644
--- a/tests/e2e/repo-settings.test.e2e.ts
+++ b/tests/e2e/repo-settings.test.e2e.ts
@@ -7,16 +7,13 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {test, save_visual} from './utils_e2e.ts';
import {validate_form} from './shared/forms.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('repo webhook settings', async ({browser}, workerInfo) => {
+test('repo webhook settings', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/repo1/settings/hooks/forgejo/new');
expect(response?.status()).toBe(200);
@@ -35,9 +32,8 @@ test('repo webhook settings', async ({browser}, workerInfo) => {
});
test.describe('repo branch protection settings', () => {
- test('form', async ({browser}, workerInfo) => {
- test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual');
- const page = await login({browser}, workerInfo);
+ test('form', async ({page}, {project}) => {
+ test.skip(project.name === 'Mobile Safari', 'Cannot get it to work - as usual');
const response = await page.goto('/user2/repo1/settings/branches/edit');
expect(response?.status()).toBe(200);
@@ -56,8 +52,7 @@ test.describe('repo branch protection settings', () => {
await save_visual(page);
});
- test.afterEach(async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+ test.afterEach(async ({page}) => {
// delete the rule for the next test
await page.goto('/user2/repo1/settings/branches/');
await page.waitForLoadState('domcontentloaded');
diff --git a/tests/e2e/repo-wiki.test.e2e.ts b/tests/e2e/repo-wiki.test.e2e.ts
index f32fe3f..4ce66da 100644
--- a/tests/e2e/repo-wiki.test.e2e.ts
+++ b/tests/e2e/repo-wiki.test.e2e.ts
@@ -4,7 +4,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
for (const searchTerm of ['space', 'consectetur']) {
for (const width of [null, 2560, 4000]) {
@@ -23,6 +23,7 @@ for (const searchTerm of ['space', 'consectetur']) {
await page.getByPlaceholder('Search wiki').dispatchEvent('keyup');
// timeout is necessary because HTMX search could be slow
await expect(page.locator('#wiki-search a[href]')).toBeInViewport({ratio: 1});
+ await save_visual(page);
});
}
}
@@ -36,4 +37,5 @@ 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);
});
diff --git a/tests/e2e/right-settings-button.test.e2e.ts b/tests/e2e/right-settings-button.test.e2e.ts
index bfb1800..3bea329 100644
--- a/tests/e2e/right-settings-button.test.e2e.ts
+++ b/tests/e2e/right-settings-button.test.e2e.ts
@@ -5,19 +5,12 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, login_user, load_logged_in_context} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
-
-test.describe('desktop viewport', () => {
- test.use({viewport: {width: 1920, height: 300}});
-
- test('Settings button on right of repo header', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+test.describe('desktop viewport as user 2', () => {
+ test.use({user: 'user2', viewport: {width: 1920, height: 300}});
+ test('Settings button on right of repo header', async ({page}) => {
await page.goto('/user2/repo1');
const settingsBtn = page.locator('.overflow-menu-items>#settings-btn');
@@ -27,11 +20,21 @@ test.describe('desktop viewport', () => {
await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
});
- test('Settings button on right of repo header also when add more button is shown', async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user12');
- const context = await load_logged_in_context(browser, workerInfo, 'user12');
- const page = await context.newPage();
+ test('Settings button on right of org header', async ({page}) => {
+ await page.goto('/org3');
+ const settingsBtn = page.locator('.overflow-menu-items>#settings-btn');
+ await expect(settingsBtn).toBeVisible();
+ await expect(settingsBtn).toHaveClass(/right/);
+
+ await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
+ });
+});
+
+test.describe('desktop viewport as user12', () => {
+ test.use({user: 'user12', viewport: {width: 1920, height: 300}});
+
+ test('Settings button on right of repo header also when add more button is shown', async ({page}) => {
await page.goto('/user12/repo10');
const settingsBtn = page.locator('.overflow-menu-items>#settings-btn');
@@ -40,19 +43,10 @@ test.describe('desktop viewport', () => {
await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
});
+});
- test('Settings button on right of org header', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
-
- await page.goto('/org3');
-
- const settingsBtn = page.locator('.overflow-menu-items>#settings-btn');
- await expect(settingsBtn).toBeVisible();
- await expect(settingsBtn).toHaveClass(/right/);
-
- await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
- });
+test.describe('desktop viewport, unauthenticated', () => {
+ test.use({viewport: {width: 1920, height: 300}});
test('User overview overflow menu should not be influenced', async ({page}) => {
await page.goto('/user2');
@@ -60,16 +54,14 @@ test.describe('desktop viewport', () => {
await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0);
await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
+ await save_visual(page);
});
});
test.describe('small viewport', () => {
- test.use({viewport: {width: 800, height: 300}});
-
- test('Settings button in overflow menu of repo header', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+ test.use({user: 'user2', viewport: {width: 800, height: 300}});
+ test('Settings button in overflow menu of repo header', async ({page}) => {
await page.goto('/user2/repo1');
await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0);
@@ -87,12 +79,10 @@ test.describe('small viewport', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
+ await save_visual(page);
});
- test('Settings button in overflow menu of org header', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
-
+ test('Settings button in overflow menu of org header', async ({page}) => {
await page.goto('/org3');
await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0);
@@ -111,6 +101,10 @@ test.describe('small viewport', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
});
+});
+
+test.describe('small viewport, unauthenticated', () => {
+ test.use({viewport: {width: 800, height: 300}});
test('User overview overflow menu should not be influenced', async ({page}) => {
await page.goto('/user2');
@@ -129,5 +123,6 @@ test.describe('small viewport', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
+ await save_visual(page);
});
});
diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts
index 7e25441..ff921a2 100644
--- a/tests/e2e/utils_e2e.ts
+++ b/tests/e2e/utils_e2e.ts
@@ -1,21 +1,34 @@
import {expect, test as baseTest, type Browser, type BrowserContextOptions, type APIRequestContext, type TestInfo, type Page} from '@playwright/test';
-export const test = baseTest.extend({
- context: async ({browser}, use) => {
- return use(await test_context(browser));
- },
- // see https://playwright.dev/docs/test-fixtures#adding-global-beforeeachaftereach-hooks
- forEachTest: [async ({page}, use) => {
- await use();
- // some tests create a new page which is not yet available here
- // only operate on tests that make the URL available
- if (page.url() !== 'about:blank') {
- await save_visual(page);
+import * as path from 'node:path';
+
+const AUTH_PATH = 'tests/e2e/.auth';
+
+type AuthScope = 'logout' | 'shared' | 'webauthn';
+
+export type TestOptions = {
+ forEachTest: void
+ user: string | null;
+ authScope: AuthScope;
+};
+
+export const test = baseTest.extend
({
+ context: async ({browser, user, authScope, contextOptions}, use, {project}) => {
+ if (user && authScope) {
+ const browserName = project.name.toLowerCase().replace(' ', '-');
+ contextOptions.storageState = path.join(AUTH_PATH, `state-${browserName}-${user}-${authScope}.json`);
+ } else {
+ // if no user is given, ensure to have clean state
+ contextOptions.storageState = {cookies: [], origins: []};
}
- }, {auto: true}],
+
+ return use(await test_context(browser, contextOptions));
+ },
+ user: null,
+ authScope: 'shared',
});
-async function test_context(browser: Browser, options?: BrowserContextOptions) {
+export async function test_context(browser: Browser, options?: BrowserContextOptions) {
const context = await browser.newContext(options);
context.on('page', (page) => {
@@ -106,6 +119,7 @@ export async function save_visual(page: Page) {
// 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-"]'),
],
diff --git a/tests/e2e/utils_e2e_test.go b/tests/e2e/utils_e2e_test.go
index bf1a8a4..96fd905 100644
--- a/tests/e2e/utils_e2e_test.go
+++ b/tests/e2e/utils_e2e_test.go
@@ -5,17 +5,27 @@ package e2e
import (
"context"
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
"net"
"net/http"
"net/url"
"os"
+ "path/filepath"
"regexp"
+ "strings"
"testing"
"time"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/json"
+ modules_session "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"
+ "code.forgejo.org/go-chi/session"
"github.com/stretchr/testify/require"
)
@@ -25,6 +35,8 @@ func onForgejoRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare .
if len(prepare) == 0 || prepare[0] {
defer tests.PrepareTestEnv(t, 1)()
}
+ createSessions(t)
+
s := http.Server{
Handler: testE2eWebRoutes,
}
@@ -64,3 +76,118 @@ func onForgejoRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ...
callback(t.(*testing.T), u)
}, prepare...)
}
+
+func createSessions(t testing.TB) {
+ t.Helper()
+ // copied from playwright.config.ts
+ browsers := []string{
+ "chromium",
+ "firefox",
+ "webkit",
+ "Mobile Chrome",
+ "Mobile Safari",
+ }
+ scopes := []string{
+ "shared",
+ }
+ users := []string{
+ "user1",
+ "user2",
+ "user12",
+ "user40",
+ }
+
+ authState := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", ".auth")
+ err := os.RemoveAll(authState)
+ require.NoError(t, err)
+
+ err = os.MkdirAll(authState, os.ModePerm)
+ require.NoError(t, err)
+
+ createSessionCookie := stateHelper(t)
+
+ for _, user := range users {
+ u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: strings.ToLower(user)})
+ for _, browser := range browsers {
+ for _, scope := range scopes {
+ stateFile := strings.ReplaceAll(strings.ToLower(fmt.Sprintf("state-%s-%s-%s.json", browser, user, scope)), " ", "-")
+ createSessionCookie(filepath.Join(authState, stateFile), u)
+ }
+ }
+ }
+}
+
+func stateHelper(t testing.TB) func(stateFile string, user *user_model.User) {
+ type Cookie struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ Domain string `json:"domain"`
+ Path string `json:"path"`
+ Expires int `json:"expires"`
+ HTTPOnly bool `json:"httpOnly"`
+ Secure bool `json:"secure"`
+ SameSite string `json:"sameSite"`
+ }
+
+ type BrowserState struct {
+ Cookies []Cookie `json:"cookies"`
+ Origins []string `json:"origins"`
+ }
+
+ options := session.Options{
+ Provider: setting.SessionConfig.Provider,
+ ProviderConfig: setting.SessionConfig.ProviderConfig,
+ CookieName: setting.SessionConfig.CookieName,
+ CookiePath: setting.SessionConfig.CookiePath,
+ Gclifetime: setting.SessionConfig.Gclifetime,
+ Maxlifetime: setting.SessionConfig.Maxlifetime,
+ Secure: setting.SessionConfig.Secure,
+ SameSite: setting.SessionConfig.SameSite,
+ Domain: setting.SessionConfig.Domain,
+ }
+
+ opt := session.PrepareOptions([]session.Options{options})
+
+ vsp := modules_session.VirtualSessionProvider{}
+ err := vsp.Init(opt.Maxlifetime, opt.ProviderConfig)
+ require.NoError(t, err)
+
+ return func(stateFile string, user *user_model.User) {
+ buf := make([]byte, opt.IDLength/2)
+ _, err = rand.Read(buf)
+ require.NoError(t, err)
+
+ sessionID := hex.EncodeToString(buf)
+
+ s, err := vsp.Read(sessionID)
+ require.NoError(t, err)
+
+ err = s.Set("uid", user.ID)
+ require.NoError(t, err)
+
+ err = s.Release()
+ require.NoError(t, err)
+
+ state := BrowserState{
+ Cookies: []Cookie{
+ {
+ Name: opt.CookieName,
+ Value: sessionID,
+ Domain: setting.Domain,
+ Path: "/",
+ Expires: -1,
+ HTTPOnly: true,
+ Secure: false,
+ SameSite: "Lax",
+ },
+ },
+ Origins: []string{},
+ }
+
+ jsonData, err := json.Marshal(state)
+ require.NoError(t, err)
+
+ err = os.WriteFile(stateFile, jsonData, 0o644)
+ require.NoError(t, err)
+ }
+}
diff --git a/tests/integration/actions_variables_test.go b/tests/integration/actions_variables_test.go
new file mode 100644
index 0000000..0179a54
--- /dev/null
+++ b/tests/integration/actions_variables_test.go
@@ -0,0 +1,150 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ forgejo_context "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestActionVariablesModification(t *testing.T) {
+ defer tests.AddFixtures("tests/integration/fixtures/TestActionVariablesModification")()
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ userVariable := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: 1001, OwnerID: user.ID})
+ userURL := "/user/settings/actions/variables"
+ org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
+ orgVariable := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: 1002, OwnerID: org.ID})
+ orgURL := "/org/" + org.Name + "/settings/actions/variables"
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user.ID})
+ repoVariable := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: 1003, RepoID: repo.ID})
+ repoURL := "/" + repo.FullName() + "/settings/actions/variables"
+ admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
+ globalVariable := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: 1004}, "owner_id = 0 AND repo_id = 0")
+ adminURL := "/admin/actions/variables"
+
+ adminSess := loginUser(t, admin.Name)
+ adminCSRF := GetCSRF(t, adminSess, "/")
+ sess := loginUser(t, user.Name)
+ csrf := GetCSRF(t, sess, "/")
+
+ type errorJSON struct {
+ Error string `json:"errorMessage"`
+ }
+
+ test := func(t *testing.T, fail bool, baseURL string, id int64) {
+ defer tests.PrintCurrentTest(t, 1)()
+ t.Helper()
+
+ sess := sess
+ csrf := csrf
+ if baseURL == adminURL {
+ sess = adminSess
+ csrf = adminCSRF
+ }
+
+ req := NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d/edit", id), map[string]string{
+ "_csrf": csrf,
+ "name": "glados_quote",
+ "data": "I'm fine. Two plus two is...ten, in base four, I'm fine!",
+ })
+ if fail {
+ resp := sess.MakeRequest(t, req, http.StatusBadRequest)
+ var error errorJSON
+ DecodeJSON(t, resp, &error)
+ assert.EqualValues(t, "Failed to find the variable.", error.Error)
+ } else {
+ sess.MakeRequest(t, req, http.StatusOK)
+ flashCookie := sess.GetCookie(forgejo_context.CookieNameFlash)
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DThe%2Bvariable%2Bhas%2Bbeen%2Bedited.", flashCookie.Value)
+ }
+
+ req = NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d/delete", id), map[string]string{
+ "_csrf": csrf,
+ })
+ if fail {
+ resp := sess.MakeRequest(t, req, http.StatusBadRequest)
+ var error errorJSON
+ DecodeJSON(t, resp, &error)
+ assert.EqualValues(t, "Failed to find the variable.", error.Error)
+ } else {
+ sess.MakeRequest(t, req, http.StatusOK)
+ flashCookie := sess.GetCookie(forgejo_context.CookieNameFlash)
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DThe%2Bvariable%2Bhas%2Bbeen%2Bremoved.", flashCookie.Value)
+ }
+ }
+
+ t.Run("User variable", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, userVariable.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, userVariable.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, true, adminURL, userVariable.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, false, userURL, userVariable.ID)
+ })
+ })
+
+ t.Run("Organisation variable", func(t *testing.T) {
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, orgVariable.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, orgVariable.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, true, adminURL, userVariable.ID)
+ })
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, false, orgURL, orgVariable.ID)
+ })
+ })
+
+ t.Run("Repository variable", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, repoVariable.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, repoVariable.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, true, adminURL, userVariable.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, false, repoURL, repoVariable.ID)
+ })
+ })
+
+ t.Run("Global variable", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, globalVariable.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, globalVariable.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, globalVariable.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, false, adminURL, globalVariable.ID)
+ })
+ })
+}
diff --git a/tests/integration/api_token_test.go b/tests/integration/api_token_test.go
index 01d18ef..f94a098 100644
--- a/tests/integration/api_token_test.go
+++ b/tests/integration/api_token_test.go
@@ -30,6 +30,23 @@ func TestAPICreateAndDeleteToken(t *testing.T) {
deleteAPIAccessToken(t, newAccessToken, user)
}
+func TestAPIGetTokens(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+ // with basic auth...
+ req := NewRequest(t, "GET", "/api/v1/users/user2/tokens").
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ // ... or with a token.
+ newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
+ req = NewRequest(t, "GET", "/api/v1/users/user2/tokens").
+ AddTokenAuth(newAccessToken.Token)
+ MakeRequest(t, req, http.StatusOK)
+ deleteAPIAccessToken(t, newAccessToken, user)
+}
+
// TestAPIDeleteMissingToken ensures that error is thrown when token not found
func TestAPIDeleteMissingToken(t *testing.T) {
defer tests.PrepareTestEnv(t)()
diff --git a/tests/integration/explore_org_test.go b/tests/integration/explore_org_test.go
new file mode 100644
index 0000000..e0c48cc
--- /dev/null
+++ b/tests/integration/explore_org_test.go
@@ -0,0 +1,49 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestExploreOrg(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // Set the default sort order
+ defer test.MockVariableValue(&setting.UI.ExploreDefaultSort, "alphabetically")()
+
+ cases := []struct{ sortOrder, expected string }{
+ {"", "?sort=" + setting.UI.ExploreDefaultSort + "&q="},
+ {"newest", "?sort=newest&q="},
+ {"oldest", "?sort=oldest&q="},
+ {"alphabetically", "?sort=alphabetically&q="},
+ {"reversealphabetically", "?sort=reversealphabetically&q="},
+ }
+ for _, c := range cases {
+ 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")
+ assert.Equal(t, c.expected, href)
+ }
+
+ // these sort orders shouldn't be supported, to avoid leaking user activity
+ cases404 := []string{
+ "/explore/organizations?sort=mostMembers",
+ "/explore/organizations?sort=leastGroups",
+ "/explore/organizations?sort=leastupdate",
+ "/explore/organizations?sort=reverseleastupdate",
+ }
+ for _, c := range cases404 {
+ req := NewRequest(t, "GET", c).SetHeader("Accept", "text/html")
+ MakeRequest(t, req, http.StatusNotFound)
+ }
+}
diff --git a/tests/integration/explore_user_test.go b/tests/integration/explore_user_test.go
index 441d89c..d1e3fd8 100644
--- a/tests/integration/explore_user_test.go
+++ b/tests/integration/explore_user_test.go
@@ -7,6 +7,8 @@ import (
"net/http"
"testing"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -15,8 +17,11 @@ import (
func TestExploreUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
+ // Set the default sort order
+ defer test.MockVariableValue(&setting.UI.ExploreDefaultSort, "reversealphabetically")()
+
cases := []struct{ sortOrder, expected string }{
- {"", "?sort=newest&q="},
+ {"", "?sort=" + setting.UI.ExploreDefaultSort + "&q="},
{"newest", "?sort=newest&q="},
{"oldest", "?sort=oldest&q="},
{"alphabetically", "?sort=alphabetically&q="},
diff --git a/tests/integration/fixtures/TestActionVariablesModification/action_variable.yml b/tests/integration/fixtures/TestActionVariablesModification/action_variable.yml
new file mode 100644
index 0000000..925838d
--- /dev/null
+++ b/tests/integration/fixtures/TestActionVariablesModification/action_variable.yml
@@ -0,0 +1,31 @@
+-
+ id: 1001
+ name: GLADOS_QUOTE
+ owner_id: 2
+ repo_id: 0
+ data: ""
+ created_unix: 1737000000
+
+-
+ id: 1002
+ name: GLADOS_QUOTE
+ owner_id: 3
+ repo_id: 0
+ data: ""
+ created_unix: 1737000001
+
+-
+ id: 1003
+ name: GLADOS_QUOTE
+ owner_id: 0
+ repo_id: 1
+ data: ""
+ created_unix: 1737000002
+
+-
+ id: 1004
+ name: GLADOS_QUOTE
+ owner_id: 0
+ repo_id: 0
+ data: ""
+ created_unix: 1737000003
diff --git a/tests/integration/fixtures/TestRunnerModification/action_runner.yml b/tests/integration/fixtures/TestRunnerModification/action_runner.yml
new file mode 100644
index 0000000..95599b1
--- /dev/null
+++ b/tests/integration/fixtures/TestRunnerModification/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/issue_test.go b/tests/integration/issue_test.go
index 4c0a64a..3cdb0b8 100644
--- a/tests/integration/issue_test.go
+++ b/tests/integration/issue_test.go
@@ -1336,3 +1336,46 @@ func TestIssueCount(t *testing.T) {
allCount := htmlDoc.doc.Find("a[data-test-name='all-issue-count']").Text()
assert.Contains(t, allCount, "2\u00a0All")
}
+
+func TestIssuePostersSearch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ type userSearchInfo struct {
+ UserID int64 `json:"user_id"`
+ UserName string `json:"username"`
+ }
+
+ type userSearchResponse struct {
+ Results []*userSearchInfo `json:"results"`
+ }
+
+ t.Run("Name search", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ defer test.MockVariableValue(&setting.UI.DefaultShowFullName, false)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/issues/posters?q=USer2")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var data userSearchResponse
+ DecodeJSON(t, resp, &data)
+
+ assert.Len(t, data.Results, 1)
+ assert.EqualValues(t, "user2", data.Results[0].UserName)
+ assert.EqualValues(t, 2, data.Results[0].UserID)
+ })
+
+ t.Run("Full name search", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ defer test.MockVariableValue(&setting.UI.DefaultShowFullName, true)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/issues/posters?q=OnE")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var data userSearchResponse
+ DecodeJSON(t, resp, &data)
+
+ assert.Len(t, data.Results, 1)
+ assert.EqualValues(t, "user1", data.Results[0].UserName)
+ assert.EqualValues(t, 1, data.Results[0].UserID)
+ })
+}
diff --git a/tests/integration/private_project_test.go b/tests/integration/private_project_test.go
new file mode 100644
index 0000000..1a4adb4
--- /dev/null
+++ b/tests/integration/private_project_test.go
@@ -0,0 +1,84 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package integration
+
+import (
+ "net/http"
+ "strings"
+ "testing"
+
+ org_model "code.gitea.io/gitea/models/organization"
+ project_model "code.gitea.io/gitea/models/project"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPrivateIssueProject(t *testing.T) {
+ defer tests.AddFixtures("models/fixtures/PrivateIssueProjects/")()
+ defer tests.PrepareTestEnv(t)()
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ sess := loginUser(t, user2.Name)
+
+ test := func(t *testing.T, sess *TestSession, username string, projectID int64, hasAccess bool) {
+ t.Helper()
+ defer tests.PrintCurrentTest(t, 1)()
+
+ // Test that the projects overview page shows the correct open and close issues.
+ req := NewRequestf(t, "GET", "%s/-/projects", username)
+ resp := sess.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ openCloseStats := htmlDoc.Find(".milestone-toolbar .group").First().Text()
+ if hasAccess {
+ assert.Contains(t, openCloseStats, "1\u00a0Open")
+ } else {
+ assert.Contains(t, openCloseStats, "0\u00a0Open")
+ }
+ assert.Contains(t, openCloseStats, "0\u00a0Closed")
+
+ // Check that on the project itself the issue is not shown.
+ req = NewRequestf(t, "GET", "%s/-/projects/%d", username, projectID)
+ resp = sess.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ htmlDoc.AssertElement(t, ".project-column .issue-card", hasAccess)
+
+ // And that the issue count is correct.
+ issueCount := strings.TrimSpace(htmlDoc.Find(".project-column-issue-count").Text())
+ if hasAccess {
+ assert.EqualValues(t, "1", issueCount)
+ } else {
+ assert.EqualValues(t, "0", issueCount)
+ }
+ }
+
+ t.Run("Organization project", func(t *testing.T) {
+ org := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: 3})
+ orgProject := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1001, OwnerID: org.ID})
+
+ t.Run("Authenticated user", func(t *testing.T) {
+ test(t, sess, org.Name, orgProject.ID, true)
+ })
+
+ t.Run("Anonymous user", func(t *testing.T) {
+ test(t, emptyTestSession(t), org.Name, orgProject.ID, false)
+ })
+ })
+
+ t.Run("User project", func(t *testing.T) {
+ userProject := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1002, OwnerID: user2.ID})
+
+ t.Run("Authenticated user", func(t *testing.T) {
+ test(t, sess, user2.Name, userProject.ID, true)
+ })
+
+ t.Run("Anonymous user", func(t *testing.T) {
+ test(t, emptyTestSession(t), user2.Name, userProject.ID, false)
+ })
+ })
+}
diff --git a/tests/integration/pull_icon_test.go b/tests/integration/pull_icon_test.go
index 8fde547..b678550 100644
--- a/tests/integration/pull_icon_test.go
+++ b/tests/integration/pull_icon_test.go
@@ -133,7 +133,7 @@ func testPullRequestListIcon(t *testing.T, doc *HTMLDoc, name, expectedColor, ex
}
func createOpenPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "open")
+ pull := createPullRequest(t, user, repo, "branch-open", "open")
assert.False(t, pull.Issue.IsClosed)
assert.False(t, pull.HasMerged)
@@ -143,7 +143,7 @@ func createOpenPullRequest(ctx context.Context, t *testing.T, user *user_model.U
}
func createOpenWipPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "open-wip")
+ pull := createPullRequest(t, user, repo, "branch-open-wip", "open-wip")
err := issue_service.ChangeTitle(ctx, pull.Issue, user, "WIP: "+pull.Issue.Title)
require.NoError(t, err)
@@ -156,7 +156,7 @@ func createOpenWipPullRequest(ctx context.Context, t *testing.T, user *user_mode
}
func createClosedPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "closed")
+ pull := createPullRequest(t, user, repo, "branch-closed", "closed")
err := issue_service.ChangeStatus(ctx, pull.Issue, user, "", true)
require.NoError(t, err)
@@ -169,7 +169,7 @@ func createClosedPullRequest(ctx context.Context, t *testing.T, user *user_model
}
func createClosedWipPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "closed-wip")
+ pull := createPullRequest(t, user, repo, "branch-closed-wip", "closed-wip")
err := issue_service.ChangeTitle(ctx, pull.Issue, user, "WIP: "+pull.Issue.Title)
require.NoError(t, err)
@@ -185,7 +185,7 @@ func createClosedWipPullRequest(ctx context.Context, t *testing.T, user *user_mo
}
func createMergedPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "merged")
+ pull := createPullRequest(t, user, repo, "branch-merged", "merged")
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
defer gitRepo.Close()
@@ -202,10 +202,7 @@ func createMergedPullRequest(ctx context.Context, t *testing.T, user *user_model
return pull
}
-func createPullRequest(t *testing.T, user *user_model.User, repo *repo_model.Repository, name string) *issues_model.PullRequest {
- branch := "branch-" + name
- title := "Testing " + name
-
+func createPullRequest(t *testing.T, user *user_model.User, repo *repo_model.Repository, branch, title string) *issues_model.PullRequest {
_, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index 1319db2..e1db171 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -518,7 +518,7 @@ func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "a-test-branch", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3])
- testIssueClose(t, user1Session, elem[1], elem[2], elem[4])
+ testIssueClose(t, user1Session, elem[1], elem[2], elem[4], true)
// Get the commit SHA
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
@@ -579,8 +579,12 @@ func testSubmitReview(t *testing.T, session *TestSession, csrf, owner, repo, pul
return session.MakeRequest(t, req, expectedSubmitStatus)
}
-func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string) *httptest.ResponseRecorder {
- req := NewRequest(t, "GET", path.Join(owner, repo, "pulls", issueNumber))
+func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string, isPull bool) *httptest.ResponseRecorder {
+ issueType := "issues"
+ if isPull {
+ issueType = "pulls"
+ }
+ req := NewRequest(t, "GET", path.Join(owner, repo, issueType, issueNumber))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index 90fc19c..01d9058 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -1462,3 +1462,15 @@ func TestRepoSubmoduleView(t *testing.T) {
htmlDoc.AssertElement(t, fmt.Sprintf(`tr[data-entryname="repo1"] a[href="%s"]`, u.JoinPath("/user2/repo1").String()), true)
})
}
+
+func TestBlameDirectory(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // Ensure directory exists.
+ req := NewRequest(t, "GET", "/user2/repo59/src/branch/master/deep")
+ MakeRequest(t, req, http.StatusOK)
+
+ // Blame is not allowed
+ req = NewRequest(t, "GET", "/user2/repo59/blame/branch/master/deep")
+ MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/runner_test.go b/tests/integration/runner_test.go
new file mode 100644
index 0000000..bab2a67
--- /dev/null
+++ b/tests/integration/runner_test.go
@@ -0,0 +1,130 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ forgejo_context "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRunnerModification(t *testing.T) {
+ defer tests.AddFixtures("tests/integration/fixtures/TestRunnerModification")()
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ userRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1001, OwnerID: user.ID})
+ userURL := "/user/settings/actions/runners"
+ org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
+ orgRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1002, OwnerID: org.ID})
+ orgURL := "/org/" + org.Name + "/settings/actions/runners"
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user.ID})
+ repoRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1003, RepoID: repo.ID})
+ repoURL := "/" + repo.FullName() + "/settings/actions/runners"
+ admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
+ globalRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1004}, "owner_id = 0 AND repo_id = 0")
+ adminURL := "/admin/actions/runners"
+
+ adminSess := loginUser(t, admin.Name)
+ adminCSRF := GetCSRF(t, adminSess, "/")
+ sess := loginUser(t, user.Name)
+ csrf := GetCSRF(t, sess, "/")
+
+ test := func(t *testing.T, fail bool, baseURL string, id int64) {
+ defer tests.PrintCurrentTest(t, 1)()
+ t.Helper()
+
+ sess := sess
+ csrf := csrf
+ if baseURL == adminURL {
+ sess = adminSess
+ csrf = adminCSRF
+ }
+
+ req := NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d", id), map[string]string{
+ "_csrf": csrf,
+ "description": "New Description",
+ })
+ if fail {
+ sess.MakeRequest(t, req, http.StatusNotFound)
+ } else {
+ sess.MakeRequest(t, req, http.StatusSeeOther)
+ flashCookie := sess.GetCookie(forgejo_context.CookieNameFlash)
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DRunner%2Bupdated%2Bsuccessfully", flashCookie.Value)
+ }
+
+ req = NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d/delete", id), map[string]string{
+ "_csrf": csrf,
+ })
+ if fail {
+ sess.MakeRequest(t, req, http.StatusNotFound)
+ } else {
+ sess.MakeRequest(t, req, http.StatusOK)
+ flashCookie := sess.GetCookie(forgejo_context.CookieNameFlash)
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DRunner%2Bdeleted%2Bsuccessfully", flashCookie.Value)
+ }
+ }
+
+ t.Run("User runner", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, userRunner.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, userRunner.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, false, userURL, userRunner.ID)
+ })
+ })
+
+ t.Run("Organisation runner", func(t *testing.T) {
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, orgRunner.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, orgRunner.ID)
+ })
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, false, orgURL, orgRunner.ID)
+ })
+ })
+
+ t.Run("Repository runner", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, repoRunner.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, repoRunner.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, false, repoURL, repoRunner.ID)
+ })
+ })
+
+ t.Run("Global runner", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, globalRunner.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, globalRunner.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, globalRunner.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, false, adminURL, globalRunner.ID)
+ })
+ })
+}
diff --git a/tests/integration/user_dashboard_test.go b/tests/integration/user_dashboard_test.go
index abc3e06..0ed5193 100644
--- a/tests/integration/user_dashboard_test.go
+++ b/tests/integration/user_dashboard_test.go
@@ -5,12 +5,21 @@ package integration
import (
"net/http"
+ "net/url"
+ "strconv"
"strings"
"testing"
+ "code.gitea.io/gitea/models/db"
+ unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/translation"
+ issue_service "code.gitea.io/gitea/services/issue"
+ files_service "code.gitea.io/gitea/services/repository/files"
+ "code.gitea.io/gitea/tests"
+ "github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -28,3 +37,45 @@ func TestUserDashboardActionLinks(t *testing.T) {
assert.EqualValues(t, locale.TrString("new_migrate.link"), strings.TrimSpace(links.Find("a[href='/repo/migrate']").Text()))
assert.EqualValues(t, locale.TrString("new_org.link"), strings.TrimSpace(links.Find("a[href='/org/create']").Text()))
}
+
+func TestDashboardTitleRendering(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ sess := loginUser(t, user4.Name)
+
+ repo, _, f := tests.CreateDeclarativeRepo(t, user4, "",
+ []unit_model.Type{unit_model.TypePullRequests, unit_model.TypeIssues}, nil,
+ []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: "test.txt",
+ ContentReader: strings.NewReader("Just some text here"),
+ },
+ },
+ )
+ defer f()
+
+ issue := createIssue(t, user4, repo, "`:exclamation:` not rendered", "Hi there!")
+ pr := createPullRequest(t, user4, repo, "testing", "`:exclamation:` not rendered")
+
+ _, err := issue_service.CreateIssueComment(db.DefaultContext, user4, repo, issue, "hi", nil)
+ require.NoError(t, err)
+
+ _, err = issue_service.CreateIssueComment(db.DefaultContext, user4, repo, pr.Issue, "hi", nil)
+ require.NoError(t, err)
+
+ testIssueClose(t, sess, repo.OwnerName, repo.Name, strconv.Itoa(int(issue.Index)), false)
+ testIssueClose(t, sess, repo.OwnerName, repo.Name, strconv.Itoa(int(pr.Issue.Index)), true)
+
+ response := sess.MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK)
+ htmlDoc := NewHTMLParser(t, response.Body)
+
+ count := 0
+ htmlDoc.doc.Find("#activity-feed .flex-item-main .title").Each(func(i int, s *goquery.Selection) {
+ count++
+ assert.EqualValues(t, ":exclamation: not rendered", s.Text())
+ })
+
+ assert.EqualValues(t, 6, count)
+ })
+}
diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css
index 4da871d..726ac7e 100644
--- a/web_src/css/features/gitgraph.css
+++ b/web_src/css/features/gitgraph.css
@@ -23,6 +23,18 @@
#git-graph-heading {
align-items: center;
}
+
+ #git-graph-heading-left {
+ margin-right: 1rem;
+ }
+
+ #git-graph-heading h2 {
+ flex-shrink: 0;
+ }
+
+ #git-graph-container #flow-select-refs-dropdown {
+ min-width: 250px;
+ }
}
@media (max-width: 767.98px) {
@@ -34,15 +46,10 @@
#git-graph-heading-left {
margin-bottom: 1rem;
}
-
- h2,
- #flow-select-refs-dropdown {
- max-width: 100%;
- }
}
#git-graph-container #flow-select-refs-dropdown {
- min-width: 250px;
+ flex-wrap: wrap;
}
#git-graph-container #flow-select-refs-dropdown .ui.label {
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 651c69c..c3f3292 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2426,6 +2426,11 @@ details.repo-search-result summary::marker {
padding-right: 22px !important; /* normal buttons have !important paddings, so we need to override it for dropdown (Add File) icons */
}
+.repo-button-row .button strong {
+ /* Workaround where 'overflow: hidden' is clipping the y-axis, force a small amount of extra padding in the y-axis. */
+ padding: .1em 0;
+}
+
.repo-button-row input {
height: 30px;
}
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 007891a..0d8396b 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -1,5 +1,5 @@