diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 4e3da6cab4..55a0f556fc 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -6,9 +6,20 @@ ignorePatterns: - /web_src/fomantic - /public/assets/js +parser: "@typescript-eslint/parser" + parserOptions: sourceType: module ecmaVersion: latest + project: true + extraFileExtensions: [".vue"] + +settings: + import/extensions: [".js", ".ts"] + import/parsers: + "@typescript-eslint/parser": [".js", ".ts"] + import/resolver: + typescript: true plugins: - "@eslint-community/eslint-plugin-eslint-comments" @@ -103,6 +114,22 @@ overrides: - files: ["web_src/js/modules/fetch.js", "web_src/js/standalone/**/*"] rules: no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression] + - files: ["**/*.vue"] + plugins: + - eslint-plugin-vue + - eslint-plugin-vue-scoped-css + extends: + - plugin:vue/vue3-recommended + - plugin:vue-scoped-css/vue3-recommended + rules: + vue/attributes-order: [0] + vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}] + vue/max-attributes-per-line: [0] + vue/singleline-html-element-content-newline: [0] + - files: ["tests/e2e/**"] + plugins: + - eslint-plugin-playwright + extends: plugin:playwright/recommended rules: "@eslint-community/eslint-comments/disable-enable-pair": [2] @@ -264,7 +291,7 @@ rules: i/no-internal-modules: [0] i/no-mutable-exports: [0] i/no-named-as-default-member: [0] - i/no-named-as-default: [2] + i/no-named-as-default: [0] i/no-named-default: [0] i/no-named-export: [0] i/no-namespace: [0] @@ -274,7 +301,7 @@ rules: i/no-restricted-paths: [0] i/no-self-import: [2] i/no-unassigned-import: [0] - i/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$", ^vitest/]}] + i/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}] i/no-unused-modules: [2, {unusedExports: true}] i/no-useless-path-segments: [2, {commonjs: true}] i/no-webpack-loader-syntax: [2] diff --git a/Makefile b/Makefile index 51577a48f0..2a6b61348a 100644 --- a/Makefile +++ b/Makefile @@ -375,11 +375,13 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig .PHONY: lint-js lint-js: node_modules - npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) + npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) + npx tsc .PHONY: lint-js-fix lint-js-fix: node_modules - npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) --fix + npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix + npx tsc .PHONY: lint-css lint-css: node_modules diff --git a/package-lock.json b/package-lock.json index 2f7a200ed2..3102d02233 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", + "typescript": "5.5.2", "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", "vue": "3.4.29", @@ -68,13 +69,16 @@ "@stoplight/spectral-cli": "6.11.1", "@stylistic/eslint-plugin-js": "2.2.1", "@stylistic/stylelint-plugin": "2.1.2", + "@typescript-eslint/parser": "7.14.1", "@vitejs/plugin-vue": "5.0.5", "eslint": "8.57.0", + "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.0.1", "eslint-plugin-i": "2.29.1", "eslint-plugin-no-jquery": "3.0.1", "eslint-plugin-no-use-extend-native": "0.5.0", + "eslint-plugin-playwright": "1.6.2", "eslint-plugin-regexp": "2.6.0", "eslint-plugin-sonarjs": "1.0.3", "eslint-plugin-unicorn": "54.0.0", @@ -2399,15 +2403,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz", - "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.13.1", - "@typescript-eslint/types": "7.13.1", - "@typescript-eslint/typescript-estree": "7.13.1", - "@typescript-eslint/visitor-keys": "7.13.1", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4" }, "engines": { @@ -2426,6 +2431,98 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.14.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "7.13.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz", @@ -5353,6 +5450,31 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, "node_modules/eslint-module-utils": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", @@ -5701,6 +5823,30 @@ "node": ">=6.0.0" } }, + "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==", + "dev": true, + "workspaces": [ + "examples" + ], + "dependencies": { + "globals": "^13.23.0" + }, + "engines": { + "node": ">=16.6.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0", + "eslint-plugin-jest": ">=25" + }, + "peerDependenciesMeta": { + "eslint-plugin-jest": { + "optional": true + } + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -11867,11 +12013,10 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "devOptional": true, - "peer": true, + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", + "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 2efebb8df8..ec1500fb8b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", + "typescript": "5.5.2", "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", "vue": "3.4.29", @@ -67,13 +68,16 @@ "@stoplight/spectral-cli": "6.11.1", "@stylistic/eslint-plugin-js": "2.2.1", "@stylistic/stylelint-plugin": "2.1.2", + "@typescript-eslint/parser": "7.14.1", "@vitejs/plugin-vue": "5.0.5", "eslint": "8.57.0", + "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.0.1", "eslint-plugin-i": "2.29.1", "eslint-plugin-no-jquery": "3.0.1", "eslint-plugin-no-use-extend-native": "0.5.0", + "eslint-plugin-playwright": "1.6.2", "eslint-plugin-regexp": "2.6.0", "eslint-plugin-sonarjs": "1.0.3", "eslint-plugin-unicorn": "54.0.0", diff --git a/playwright.config.js b/playwright.config.ts similarity index 81% rename from playwright.config.js rename to playwright.config.ts index bdd303ae25..d1cd299e25 100644 --- a/playwright.config.js +++ b/playwright.config.ts @@ -1,15 +1,12 @@ -// @ts-check import {devices} from '@playwright/test'; +import {env} from 'node:process'; +import type {PlaywrightTestConfig} from '@playwright/test'; -const BASE_URL = process.env.GITEA_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000'; +const BASE_URL = env.GITEA_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000'; -/** - * @see https://playwright.dev/docs/test-configuration - * @type {import('@playwright/test').PlaywrightTestConfig} - */ export default { testDir: './tests/e2e/', - testMatch: /.*\.test\.e2e\.js/, // Match any .test.e2e.js files + testMatch: /.*\.test\.e2e\.ts/, // Match any .test.e2e.ts files /* Maximum time one test can run for. */ timeout: 30 * 1000, @@ -24,13 +21,13 @@ export default { }, /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: Boolean(process.env.CI), + forbidOnly: Boolean(env.CI), /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, + retries: env.CI ? 2 : 0, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? 'list' : [['list'], ['html', {outputFolder: 'tests/e2e/reports/', open: 'never'}]], + reporter: env.CI ? 'list' : [['list'], ['html', {outputFolder: 'tests/e2e/reports/', open: 'never'}]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { @@ -98,4 +95,4 @@ export default { outputDir: 'tests/e2e/test-artifacts/', /* Folder for test artifacts such as screenshots, videos, traces, etc. */ snapshotDir: 'tests/e2e/test-snapshots/', -}; +} satisfies PlaywrightTestConfig; diff --git a/tests/e2e/README.md b/tests/e2e/README.md index e5fd1ca6c0..db083793d8 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -65,7 +65,7 @@ TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME= ## Running individual tests -Example command to run `example.test.e2e.js` test file: +Example command to run `example.test.e2e.ts` test file: _Note: unlike integration tests, this filtering is at the file level, not function_ diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 60528a1a78..d6d27e66be 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -73,10 +73,10 @@ func TestMain(m *testing.M) { os.Exit(exitVal) } -// TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.js" files in this directory and build a test for each. +// TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.ts" files in this directory and build a test for each. func TestE2e(t *testing.T) { // Find the paths of all e2e test files in test directory. - searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.js") + searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.ts") paths, err := filepath.Glob(searchGlob) if err != nil { t.Fatal(err) diff --git a/tests/e2e/example.test.e2e.js b/tests/e2e/example.test.e2e.ts similarity index 86% rename from tests/e2e/example.test.e2e.js rename to tests/e2e/example.test.e2e.ts index 57c69a2917..32813b3934 100644 --- a/tests/e2e/example.test.e2e.js +++ b/tests/e2e/example.test.e2e.ts @@ -1,19 +1,18 @@ -// @ts-check import {test, expect} from '@playwright/test'; -import {login_user, save_visual, load_logged_in_context} from './utils_e2e.js'; +import {login_user, save_visual, load_logged_in_context} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); }); -test('Load Homepage', async ({page}) => { +test('homepage', async ({page}) => { const response = await page.goto('/'); await expect(response?.status()).toBe(200); // Status OK await expect(page).toHaveTitle(/^Gitea: Git with a cup of tea\s*$/); await expect(page.locator('.logo')).toHaveAttribute('src', '/assets/img/logo.svg'); }); -test('Test Register Form', async ({page}, workerInfo) => { +test('register', async ({page}, workerInfo) => { const response = await page.goto('/user/sign_up'); await expect(response?.status()).toBe(200); // Status OK await page.type('input[name=user_name]', `e2e-test-${workerInfo.workerIndex}`); @@ -29,7 +28,7 @@ test('Test Register Form', async ({page}, workerInfo) => { save_visual(page); }); -test('Test Login Form', async ({page}, workerInfo) => { +test('login', async ({page}, workerInfo) => { const response = await page.goto('/user/login'); await expect(response?.status()).toBe(200); // Status OK @@ -37,14 +36,14 @@ test('Test Login Form', async ({page}, workerInfo) => { await page.type('input[name=password]', `password`); await page.click('form button.ui.primary.button:visible'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); save_visual(page); }); -test('Test Logged In User', async ({browser}, workerInfo) => { +test('logged in user', async ({browser}, workerInfo) => { const context = await load_logged_in_context(browser, workerInfo, 'user2'); const page = await context.newPage(); diff --git a/tests/e2e/utils_e2e.js b/tests/e2e/utils_e2e.ts similarity index 88% rename from tests/e2e/utils_e2e.js rename to tests/e2e/utils_e2e.ts index d60c78b16e..5678c9c9d0 100644 --- a/tests/e2e/utils_e2e.js +++ b/tests/e2e/utils_e2e.ts @@ -1,4 +1,5 @@ import {expect} from '@playwright/test'; +import {env} from 'node:process'; const ARTIFACTS_PATH = `tests/e2e/test-artifacts`; const LOGIN_PASSWORD = 'password'; @@ -20,7 +21,7 @@ export async function login_user(browser, workerInfo, user) { await page.type('input[name=password]', LOGIN_PASSWORD); await page.click('form button.ui.primary.button:visible'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle await expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`); @@ -44,8 +45,8 @@ export async function load_logged_in_context(browser, workerInfo, user) { export async function save_visual(page) { // Optionally include visual testing - if (process.env.VISUAL_TEST) { - await page.waitForLoadState('networkidle'); + if (env.VISUAL_TEST) { + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle // Mock page/version string await page.locator('footer div.ui.left').evaluate((node) => node.innerHTML = 'MOCK'); await expect(page).toHaveScreenshot({ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..7ddbada765 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "include": [ + "*", + "tests/e2e/**/*", + "tools/**/*", + "web_src/js/**/*", + ], + "compilerOptions": { + "target": "es2020", + "module": "node16", + "moduleResolution": "node16", + "lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"], + "allowImportingTsExtensions": true, + "allowJs": true, + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "esModuleInterop": true, + "isolatedModules": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "stripInternal": true, + "strict": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": false, + "exactOptionalPropertyTypes": false, + } +} diff --git a/vitest.config.js b/vitest.config.ts similarity index 100% rename from vitest.config.js rename to vitest.config.ts diff --git a/web_src/js/components/.eslintrc.yaml b/web_src/js/components/.eslintrc.yaml deleted file mode 100644 index a79e96f330..0000000000 --- a/web_src/js/components/.eslintrc.yaml +++ /dev/null @@ -1,22 +0,0 @@ -plugins: - - eslint-plugin-vue - - eslint-plugin-vue-scoped-css - -extends: - - ../../../.eslintrc.yaml - - plugin:vue/vue3-recommended - - plugin:vue-scoped-css/vue3-recommended - -parserOptions: - sourceType: module - ecmaVersion: latest - -env: - browser: true - -rules: - vue/attributes-order: [0] - vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}] - vue/max-attributes-per-line: [0] - vue/singleline-html-element-content-newline: [0] - vue-scoped-css/enforce-style-type: [0] diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 7f6524c7e3..97dc0d950f 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -797,7 +797,7 @@ export function initRepositoryActionView() { } -