diff --git a/eslint.config.mjs b/eslint.config.mjs
index 389f7c3cd1..ee2aa5da27 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1125,9 +1125,11 @@ export default [{
         allowConditional: true,
       },
     ],
+    'playwright/no-useless-await': [2],
 
     'playwright/prefer-comparison-matcher': [2],
     'playwright/prefer-equality-matcher': [2],
+    'playwright/prefer-native-locators': [2],
     'playwright/prefer-to-contain': [2],
     'playwright/prefer-to-have-length': [2],
     'playwright/require-to-throw-message': [2],
diff --git a/package-lock.json b/package-lock.json
index ed6ca6f647..c974973366 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -75,7 +75,7 @@
         "eslint-plugin-import-x": "4.3.1",
         "eslint-plugin-no-jquery": "3.0.2",
         "eslint-plugin-no-use-extend-native": "0.5.0",
-        "eslint-plugin-playwright": "1.6.2",
+        "eslint-plugin-playwright": "2.0.0",
         "eslint-plugin-regexp": "2.6.0",
         "eslint-plugin-sonarjs": "2.0.3",
         "eslint-plugin-unicorn": "56.0.0",
@@ -8294,9 +8294,9 @@
       }
     },
     "node_modules/eslint-plugin-playwright": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.2.tgz",
-      "integrity": "sha512-mraN4Em3b5jLt01q7qWPyLg0Q5v3KAWfJSlEWwldyUXoa7DSPrBR4k6B6LROLqipsG8ndkwWMdjl1Ffdh15tag==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.0.0.tgz",
+      "integrity": "sha512-nPa44nSp48mp/U+GSneabrhlyIyGvrcv+Z14u6sgno+jX8N0bH+ooSLEC1L6dvMDSHs7tj+kMIbls3l8gCJJSg==",
       "dev": true,
       "license": "MIT",
       "workspaces": [
@@ -8309,13 +8309,7 @@
         "node": ">=16.6.0"
       },
       "peerDependencies": {
-        "eslint": ">=8.40.0",
-        "eslint-plugin-jest": ">=25"
-      },
-      "peerDependenciesMeta": {
-        "eslint-plugin-jest": {
-          "optional": true
-        }
+        "eslint": ">=8.40.0"
       }
     },
     "node_modules/eslint-plugin-playwright/node_modules/globals": {
diff --git a/package.json b/package.json
index eecff8e617..4a0dfafc3e 100644
--- a/package.json
+++ b/package.json
@@ -74,7 +74,7 @@
     "eslint-plugin-import-x": "4.3.1",
     "eslint-plugin-no-jquery": "3.0.2",
     "eslint-plugin-no-use-extend-native": "0.5.0",
-    "eslint-plugin-playwright": "1.6.2",
+    "eslint-plugin-playwright": "2.0.0",
     "eslint-plugin-regexp": "2.6.0",
     "eslint-plugin-sonarjs": "2.0.3",
     "eslint-plugin-unicorn": "56.0.0",
diff --git a/tests/e2e/dashboard-ci-status.test.e2e.js b/tests/e2e/dashboard-ci-status.test.e2e.js
index ec61bfac76..289430055c 100644
--- a/tests/e2e/dashboard-ci-status.test.e2e.js
+++ b/tests/e2e/dashboard-ci-status.test.e2e.js
@@ -15,7 +15,7 @@ test('Correct link and tooltip', async ({browser}, workerInfo) => {
   const context = await load_logged_in_context(browser, workerInfo, 'user2');
   const page = await context.newPage();
   const response = await page.goto('/?repo-search-query=test_workflows');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   await page.waitForLoadState('networkidle');
 
diff --git a/tests/e2e/example.test.e2e.js b/tests/e2e/example.test.e2e.js
index c163c8bb42..a413a218c6 100644
--- a/tests/e2e/example.test.e2e.js
+++ b/tests/e2e/example.test.e2e.js
@@ -15,21 +15,21 @@ test.beforeAll(async ({browser}, workerInfo) => {
 
 test('Load Homepage', async ({page}) => {
   const response = await page.goto('/');
-  await expect(response?.status()).toBe(200); // Status OK
+  expect(response?.status()).toBe(200); // Status OK
   await expect(page).toHaveTitle(/^Forgejo: Beyond coding. We Forge.\s*$/);
   await expect(page.locator('.logo')).toHaveAttribute('src', '/assets/img/logo.svg');
 });
 
 test('Register Form', async ({page}, workerInfo) => {
   const response = await page.goto('/user/sign_up');
-  await expect(response?.status()).toBe(200); // Status OK
+  expect(response?.status()).toBe(200); // Status OK
   await page.type('input[name=user_name]', `e2e-test-${workerInfo.workerIndex}`);
   await page.type('input[name=email]', `e2e-test-${workerInfo.workerIndex}@test.com`);
   await page.type('input[name=password]', 'test123test123');
   await page.type('input[name=retype]', 'test123test123');
   await page.click('form button.ui.primary.button:visible');
   // Make sure we routed to the home page. Else login failed.
-  await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
+  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!');
 
diff --git a/tests/e2e/explore.test.e2e.js b/tests/e2e/explore.test.e2e.js
index b64eca78e3..eb0c723f36 100644
--- a/tests/e2e/explore.test.e2e.js
+++ b/tests/e2e/explore.test.e2e.js
@@ -42,5 +42,5 @@ test('Explore view taborder', async ({page}) => {
       break;
     }
   }
-  await expect(res).toBe(exp);
+  expect(res).toBe(exp);
 });
diff --git a/tests/e2e/issue-comment.test.e2e.js b/tests/e2e/issue-comment.test.e2e.js
index 55dccf1ebd..a21f00c511 100644
--- a/tests/e2e/issue-comment.test.e2e.js
+++ b/tests/e2e/issue-comment.test.e2e.js
@@ -49,7 +49,7 @@ test('Hyperlink paste behaviour', async ({browser}, workerInfo) => {
 test('Always focus edit tab first on edit', async ({browser}, workerInfo) => {
   const page = await login({browser}, workerInfo);
   const response = await page.goto('/user2/repo1/issues/1');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   // Switch to preview tab and save
   await page.click('#issue-1 .comment-container .context-menu');
diff --git a/tests/e2e/issue-sidebar.test.e2e.js b/tests/e2e/issue-sidebar.test.e2e.js
index 7fd60555be..8b5fa59331 100644
--- a/tests/e2e/issue-sidebar.test.e2e.js
+++ b/tests/e2e/issue-sidebar.test.e2e.js
@@ -39,7 +39,7 @@ test('Pull: Toggle WIP', async ({browser}, 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/pulls/5');
-  await expect(response?.status()).toBe(200); // Status OK
+  expect(response?.status()).toBe(200); // Status OK
   // initial state
   await check_wip({page}, false);
   // toggle to WIP
@@ -82,7 +82,7 @@ test('Issue: Labels', async ({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');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
   // preconditions
   await expect(labelList.filter({hasText: 'label1'})).toBeVisible();
   await expect(labelList.filter({hasText: 'label2'})).toBeHidden();
@@ -110,7 +110,7 @@ test('Issue: Assignees', async ({browser}, workerInfo) => {
   const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item a');
 
   const response = await page.goto('/org3/repo3/issues/1');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
   // preconditions
   await expect(assigneesList.filter({hasText: 'user2'})).toBeVisible();
   await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden();
@@ -153,7 +153,7 @@ test('New Issue: Assignees', async ({browser}, workerInfo) => {
   const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item');
 
   const response = await page.goto('/org3/repo3/issues/new');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
   // preconditions
   await expect(page.locator('.ui.assignees.list .item.no-select')).toBeVisible();
   await expect(assigneesList.filter({hasText: 'user2'})).toBeHidden();
@@ -191,7 +191,7 @@ test('Issue: Milestone', async ({browser}, workerInfo) => {
   const page = await login({browser}, workerInfo);
 
   const response = await page.goto('/user2/repo1/issues/1');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   const selectedMilestone = page.locator('.issue-content-right .select-milestone.list');
   const milestoneDropdown = page.locator('.issue-content-right .select-milestone.dropdown');
@@ -215,7 +215,7 @@ test('New Issue: Milestone', async ({browser}, workerInfo) => {
   const page = await login({browser}, workerInfo);
 
   const response = await page.goto('/user2/repo1/issues/new');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   const selectedMilestone = page.locator('.issue-content-right .select-milestone.list');
   const milestoneDropdown = page.locator('.issue-content-right .select-milestone.dropdown');
diff --git a/tests/e2e/markdown-editor.test.e2e.js b/tests/e2e/markdown-editor.test.e2e.js
index a1f2a2d96c..37b8fecf36 100644
--- a/tests/e2e/markdown-editor.test.e2e.js
+++ b/tests/e2e/markdown-editor.test.e2e.js
@@ -19,7 +19,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
 
   const page = await context.newPage();
   const response = await page.goto('/user2/repo1/issues/new');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   const textarea = page.locator('textarea[name=content]');
   const tab = '    ';
@@ -92,7 +92,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
 
   const page = await context.newPage();
   const response = await page.goto('/user2/repo1/issues/new');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   const textarea = page.locator('textarea[name=content]');
   const tab = '    ';
diff --git a/tests/e2e/markup.test.e2e.js b/tests/e2e/markup.test.e2e.js
index a2b795e852..f7ec31709d 100644
--- a/tests/e2e/markup.test.e2e.js
+++ b/tests/e2e/markup.test.e2e.js
@@ -9,7 +9,7 @@ import {test} from './utils_e2e.js';
 
 test('markup with #xyz-mode-only', async ({page}) => {
   const response = await page.goto('/user2/repo1/issues/1');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
   await page.waitForLoadState('networkidle');
 
   const comment = page.locator('.comment-body>.markup', {hasText: 'test markup light/dark-mode-only'});
diff --git a/tests/e2e/org-settings.test.e2e.js b/tests/e2e/org-settings.test.e2e.js
index 2a0fe69608..21f34c123d 100644
--- a/tests/e2e/org-settings.test.e2e.js
+++ b/tests/e2e/org-settings.test.e2e.js
@@ -18,7 +18,7 @@ test('org team settings', async ({browser}, 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');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   await page.locator('input[name="permission"][value="admin"]').click();
   await expect(page.locator('.hide-unless-checked')).toBeHidden();
diff --git a/tests/e2e/reaction-selectors.test.e2e.js b/tests/e2e/reaction-selectors.test.e2e.js
index 5e0ea5b519..184f25fe18 100644
--- a/tests/e2e/reaction-selectors.test.e2e.js
+++ b/tests/e2e/reaction-selectors.test.e2e.js
@@ -42,7 +42,7 @@ test('Reaction Selectors', async ({browser}, workerInfo) => {
   const page = await context.newPage();
 
   const response = await page.goto('/user2/repo1/issues/1');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   const comment = page.locator('.comment#issuecomment-2').first();
 
diff --git a/tests/e2e/repo-code.test.e2e.js b/tests/e2e/repo-code.test.e2e.js
index 9d9653d2fe..fdb92762ff 100644
--- a/tests/e2e/repo-code.test.e2e.js
+++ b/tests/e2e/repo-code.test.e2e.js
@@ -42,7 +42,7 @@ test('Line Range Selection', async ({browser}, workerInfo) => {
   const filePath = '/user2/repo1/src/branch/master/README.md?display=source';
 
   const response = await page.goto(filePath);
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   await assertSelectedLines(page, []);
   await page.locator('span#L1').click();
@@ -72,7 +72,7 @@ test('Readable diff', async ({page}, workerInfo) => {
   ];
   for (const thisDiff of expectedDiffs) {
     const response = await page.goto('/user2/diff-test/commits/branch/main');
-    await expect(response?.status()).toBe(200); // Status OK
+    expect(response?.status()).toBe(200); // Status OK
     await page.getByText(`Patch: ${thisDiff.id}`).click();
     if (thisDiff.removed) {
       await expect(page.getByText(thisDiff.removed, {exact: true})).toHaveClass(/removed-code/);
diff --git a/tests/e2e/repo-commitgraph.test.e2e.js b/tests/e2e/repo-commitgraph.test.e2e.js
index 7bb3e1f23f..f06c68a55d 100644
--- a/tests/e2e/repo-commitgraph.test.e2e.js
+++ b/tests/e2e/repo-commitgraph.test.e2e.js
@@ -24,7 +24,7 @@ test('Switch branch', async ({browser}, workerInfo) => {
   const context = await load_logged_in_context(browser, workerInfo, 'user2');
   const page = await context.newPage();
   const response = await page.goto('/user2/repo1/graph');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   await page.click('#flow-select-refs-dropdown');
   const input = page.locator('#flow-select-refs-dropdown');
diff --git a/tests/e2e/repo-migrate.test.e2e.js b/tests/e2e/repo-migrate.test.e2e.js
index 7a9fc08fb2..2ad4400340 100644
--- a/tests/e2e/repo-migrate.test.e2e.js
+++ b/tests/e2e/repo-migrate.test.e2e.js
@@ -14,7 +14,7 @@ test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo
 
   const page = await (await load_logged_in_context(browser, workerInfo, 'user2')).newPage();
 
-  await expect((await page.goto('/user2/invalidrepo'))?.status(), 'repo should not exist yet').toBe(404);
+  expect((await page.goto('/user2/invalidrepo'))?.status(), 'repo should not exist yet').toBe(404);
 
   await page.goto('/repo/migrate?service_type=1');
 
@@ -24,7 +24,7 @@ test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo
   await form.locator('button.primary').click({timeout: 5000});
   await expect(page).toHaveURL('user2/invalidrepo');
 
-  await expect((await unauthedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200);
+  expect((await unauthedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200);
   await expect(unauthedPage.locator('#repo_migrating_progress')).toBeVisible();
 
   await page.reload();
diff --git a/tests/e2e/repo-settings.test.e2e.js b/tests/e2e/repo-settings.test.e2e.js
index 226cc062d3..b4fdc1ae6c 100644
--- a/tests/e2e/repo-settings.test.e2e.js
+++ b/tests/e2e/repo-settings.test.e2e.js
@@ -20,7 +20,7 @@ test('repo webhook settings', async ({browser}, 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');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   await page.locator('input[name="events"][value="choose_events"]').click();
   await expect(page.locator('.hide-unless-checked')).toBeVisible();
@@ -39,7 +39,7 @@ test.describe('repo branch protection settings', () => {
     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/branches/edit');
-    await expect(response?.status()).toBe(200);
+    expect(response?.status()).toBe(200);
 
     await validate_form({page}, 'fieldset');
 
diff --git a/tests/e2e/shared/forms.js b/tests/e2e/shared/forms.js
index 0ffd6eef2d..5775c40826 100644
--- a/tests/e2e/shared/forms.js
+++ b/tests/e2e/shared/forms.js
@@ -26,7 +26,7 @@ export async function validate_form({page}, scope) {
   // might be necessary to adjust in case colons are strictly necessary in help text
   for (const l of await page.locator('label').all()) {
     const str = await l.textContent();
-    await expect(str.split('\n')[0]).not.toContain(':');
+    expect(str.split('\n')[0]).not.toContain(':');
   }
 
   // check that multiple help text are correctly aligned to each other
@@ -36,9 +36,9 @@ export async function validate_form({page}, scope) {
     const boxes = await Promise.all(helpLabels.map((help) => help.boundingBox()));
     for (let i = 1; i < boxes.length; i++) {
       // help texts vertically aligned on top of each other
-      await expect(boxes[i].x).toBe(boxes[0].x);
+      expect(boxes[i].x).toBe(boxes[0].x);
       // help texts don't horizontally intersect each other
-      await expect(boxes[i].y + boxes[i].height).toBeGreaterThanOrEqual(boxes[i - 1].y + boxes[i - 1].height);
+      expect(boxes[i].y + boxes[i].height).toBeGreaterThanOrEqual(boxes[i - 1].y + boxes[i - 1].height);
     }
   }
 }
diff --git a/tests/e2e/utils_e2e.js b/tests/e2e/utils_e2e.js
index 6afbe43b86..7cfd7388ca 100644
--- a/tests/e2e/utils_e2e.js
+++ b/tests/e2e/utils_e2e.js
@@ -30,7 +30,7 @@ export async function login_user(browser, workerInfo, user) {
   // Route to login page
   // Note: this could probably be done more quickly with a POST
   const response = await page.goto('/user/login');
-  await expect(response?.status()).toBe(200); // Status OK
+  expect(response?.status()).toBe(200); // Status OK
 
   // Fill out form
   await page.type('input[name=user_name]', user);
@@ -39,7 +39,7 @@ export async function login_user(browser, workerInfo, user) {
 
   await page.waitForLoadState('networkidle');
 
-  await expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`);
+  expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`);
 
   // Save state
   await context.storageState({path: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
diff --git a/tests/e2e/webauthn.test.e2e.js b/tests/e2e/webauthn.test.e2e.js
index 1f645ffea2..7168de223a 100644
--- a/tests/e2e/webauthn.test.e2e.js
+++ b/tests/e2e/webauthn.test.e2e.js
@@ -18,7 +18,7 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) =>
 
   // Register a security key.
   let response = await page.goto('/user/settings/security');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   // https://github.com/microsoft/playwright/issues/7276#issuecomment-1516768428
   const cdpSession = await page.context().newCDPSession(page);
@@ -45,7 +45,7 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) =>
 
   // Login.
   response = await page.goto('/user/login');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
 
   await page.getByLabel('Username or email address').fill(username);
   await page.getByLabel('Password').fill('password');
@@ -55,7 +55,7 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) =>
 
   // Cleanup.
   response = await page.goto('/user/settings/security');
-  await expect(response?.status()).toBe(200);
+  expect(response?.status()).toBe(200);
   await page.getByRole('button', {name: 'Remove'}).click();
   await page.getByRole('button', {name: 'Yes'}).click();
   await page.waitForURL(`${workerInfo.project.use.baseURL}/user/settings/security`);