diff --git a/.github/workflows/test-backend.yaml b/.github/workflows/test-backend.yaml index 25f67323a..fbdf68eb8 100644 --- a/.github/workflows/test-backend.yaml +++ b/.github/workflows/test-backend.yaml @@ -4,6 +4,9 @@ on: pull_request: workflow_dispatch: +env: + PYTHONPATH: ${{ github.workspace }}/backend + # defaults: # run: # working-directory: ./backend @@ -40,14 +43,16 @@ jobs: # run pytest #---------------------------------------------- - name: Run tests - run: poetry -C backend run pytest backend/tests + run: | + poetry -C backend run pytest tests + shell: bash #---------------------------------------------- # coverage report #---------------------------------------------- - name: Generate coverage results run: | - poetry -C backend run coverage run -m pytest backend/tests + poetry -C backend run coverage run -m pytest tests poetry -C backend run coverage xml poetry -C backend run coverage report -m shell: bash diff --git a/frontend/.eslintignore b/frontend/.eslintignore deleted file mode 100644 index 196990737..000000000 --- a/frontend/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -dist -mockServiceWorker.js -/src/api/model.ts diff --git a/frontend/.eslintrc b/frontend/.eslintrc deleted file mode 100644 index 702e771df..000000000 --- a/frontend/.eslintrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "root": true, - "extends": [ - "plugin:vue/vue3-recommended", - "plugin:vuejs-accessibility/recommended", - "eslint:recommended", - "@vue/eslint-config-typescript", - "@vue/eslint-config-prettier/skip-formatting", - ], - "parserOptions": { - "ecmaVersion": "latest", - }, - "rules": { - "prettier/prettier": "warn", - "vuejs-accessibility/anchor-has-content": [ - "error", - { - "accessibleDirectives": ["tooltip"], - }, - ], - "vuejs-accessibility/label-has-for": [ - "error", - { - "controlComponents": ["AppInput"], - "required": { - "some": ["nesting", "id"], - }, - "allowChildren": true, - }, - ], - "vue/no-v-html": ["off"], - "vue/no-v-text-v-html-on-component": ["off"], - "vuejs-accessibility/mouse-events-have-key-events": ["off"], - }, -} diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 5b28e2ebe..4c3801565 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/e2e/axe.test.ts b/frontend/e2e/axe.test.ts index 0fa3e91c5..481cbfcc8 100644 --- a/frontend/e2e/axe.test.ts +++ b/frontend/e2e/axe.test.ts @@ -39,12 +39,9 @@ const rules = [ { id: "region", selector: ":not([role='listbox']" }, ]; -type Test = Parameters[1]; - -/** generic page axe test */ -const checkPage = - (path: string, selector?: string): Test => - async ({ page, browserName }) => { +/** Reusable test for each page */ +const checkPage = (title: string, path: string, selector?: string) => + test(title, async ({ page, browserName }) => { test.skip(browserName !== "chromium", "Only test Axe on chromium"); /** navigate to page */ @@ -70,25 +67,27 @@ const checkPage = const violations = await getViolations(page); const violationsMessage = JSON.stringify(violations, null, 2); expect(violationsMessage).toBe("[]"); - }; + }); -/** check all pages */ -for (const path of paths) test("Accessibility check " + path, checkPage(path)); +for (const path of paths) checkPage("Accessibility check " + path, path); -/** extra testbed component tests */ -test( +checkPage( "Accessibility check /testbed (select single)", - checkPage("/testbed", ".select-single button"), + "/testbed", + ".select-single button", ); -test( +checkPage( "Accessibility check /testbed (select multi)", - checkPage("/testbed", ".select-multi button"), + "/testbed", + ".select-multi button", ); -test( +checkPage( "Accessibility check /testbed (select tags)", - checkPage("/testbed", ".select-tags input"), + "/testbed", + ".select-tags input", ); -test( +checkPage( "Accessibility check /testbed (select autocomplete)", - checkPage("/testbed", ".select-autocomplete input"), + "/testbed", + ".select-autocomplete input", ); diff --git a/frontend/e2e/text-annotator.test.ts b/frontend/e2e/text-annotator.test.ts index 9e4616237..3831151a1 100644 --- a/frontend/e2e/text-annotator.test.ts +++ b/frontend/e2e/text-annotator.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "@playwright/test"; import { log } from "../playwright.config"; /** https://github.com/microsoft/playwright/issues/23662 */ -import example from "../src/pages/explore/text-annotator.json"; +import example from "../src/pages/explore/text-annotator.json" with { type: "json" }; log(); diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 000000000..e50b117f1 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,86 @@ +import url from "url"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import pluginVue from "eslint-plugin-vue"; +import { FlatCompat } from "@eslint/eslintrc"; +import js from "@eslint/js"; +import prettierConfig from "@vue/eslint-config-prettier"; +import vueTsEslintConfig from "@vue/eslint-config-typescript"; + +const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + // allConfig: js.configs.all, +}); + +export default [ + /** Extend recommended configs */ + ...compat.extends( + "plugin:vue/vue3-recommended", + "plugin:vuejs-accessibility/recommended", + "eslint:recommended", + "@vue/eslint-config-prettier/skip-formatting", + ), + ...pluginVue.configs["flat/recommended"], + ...vueTsEslintConfig(), + eslintPluginPrettierRecommended, + prettierConfig, + { + ignores: [ + "node_modules", + "dist", + "mockServiceWorker.js", + "src/api/model.ts", + ], + }, + /** Configuration */ + { + languageOptions: { + parserOptions: { + ecmaVersion: "latest", + sourceType: "script", + }, + }, + files: [ + "**/*.js", + "**/*.cjs", + "**/*.mjs", + "**/*.ts", + "**/*.tsx", + "**/*.vue", + ], + /** Override rules */ + rules: { + "max-len": ["error", { code: 120 }], + "no-constant-binary-expression": ["off"], + "prefer-const": "off", + "@typescript-eslint/no-empty-object-type": ["warn"], + "@typescript-eslint/no-unused-expressions": ["off"], + "@typescript-eslint/no-unused-vars": ["off"], + "prettier/prettier": ["warn", {}, { usePrettierrc: true }], + "vue/attribute-hyphenation": [ + "warn", + "always", + { ignore: ["selectedFilters"] }, + ], + "vue/no-v-html": ["off"], + "vue/no-v-text-v-html-on-component": ["off"], + "vuejs-accessibility/anchor-has-content": [ + "error", + { + accessibleDirectives: ["tooltip"], + }, + ], + "vuejs-accessibility/label-has-for": [ + "error", + { + required: { + some: ["nesting", "id"], + }, + allowChildren: true, + }, + ], + "vuejs-accessibility/mouse-events-have-key-events": ["off"], + }, + }, +]; diff --git a/frontend/fixtures/index.ts b/frontend/fixtures/index.ts index 674e40af0..5a28dd18f 100644 --- a/frontend/fixtures/index.ts +++ b/frontend/fixtures/index.ts @@ -14,7 +14,7 @@ import phenotypeExplorerCompare from "./phenotype-explorer-compare.json"; import phenotypeExplorerMulticompare from "./phenotype-explorer-multi-compare.json"; import phenotypeExplorerSearch from "./phenotype-explorer-search.json"; import search from "./search.json"; -import textAnnotator from "./text-annotator.json"; +import textAnnotator from "./text-annotator.json" with { type: "json" }; import uptime from "./uptime.json"; /** api calls to be mocked with fixture data */ diff --git a/frontend/package.json b/frontend/package.json index c3e5594fe..7adf1c3b4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,71 +1,72 @@ { "name": "monarch-app", + "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", - "lint": "eslint . --ext .vue,.ts,.js,.cjs,.mjs --fix && prettier --write \"**/*.{vue,ts,js,css,scss,json,yaml,svg,md}\"", + "lint": "eslint . --fix && prettier --write \"**/*.{vue,ts,js,css,scss,json,yaml,svg,md}\"", "test": "bun run test:types && bun run test:lint && bun run test:unit && bun run test:e2e", "test:types": "vue-tsc --noEmit -p tsconfig.json --composite false", - "test:lint": "eslint . --ext .vue,.ts,.js,.cjs,.mjs && prettier --check \"**/*.{vue,ts,js,css,scss,json,yaml,svg,md}\"", + "test:lint": "eslint . && prettier --check \"**/*.{vue,ts,js,css,scss,json,yaml,svg,md}\"", "test:unit": "vitest run", "test:e2e": "playwright test" }, "dependencies": { - "@floating-ui/dom": "^1.6.3", - "@fortawesome/fontawesome-svg-core": "^6.5.1", - "@fortawesome/free-brands-svg-icons": "^6.5.1", - "@fortawesome/free-regular-svg-icons": "^6.5.1", - "@fortawesome/free-solid-svg-icons": "^6.5.1", - "@fortawesome/vue-fontawesome": "^3.0.6", - "@sentry/browser": "^7.105.0", - "@sentry/vue": "^7.105.0", - "@vueuse/core": "^10.9.0", - "apexcharts": "^3.46.0", + "@floating-ui/dom": "^1.6.13", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@fortawesome/vue-fontawesome": "^3.0.8", + "@sentry/browser": "^8.48.0", + "@sentry/vue": "^8.48.0", + "@vueuse/core": "^12.4.0", + "apexcharts": "^4.3.0", "body-scroll-lock": "4.0.0-beta.0", "lodash": "^4.17.21", - "micromark": "^4.0.0", + "micromark": "^4.0.1", "normalize.css": "^8.0.1", "tippy.js": "^6.3.7", - "ua-parser-js": "^1.0.37", - "vue": "^3.4.21", + "ua-parser-js": "^2.0.0", + "vue": "^3.5.13", "vue-gtag": "^2.0.1", "vue-hotjar": "^1.4.0", - "vue-router": "^4.3.0", - "vue-tippy": "^6.4.1", - "vue3-apexcharts": "~1.4.4" + "vue-router": "^4.5.0", + "vue-tippy": "^6.6.0", + "vue3-apexcharts": "^1.8.0" }, "devDependencies": { - "@ianvs/prettier-plugin-sort-imports": "^4.1.1", - "@playwright/test": "~1.39.0", - "@tsconfig/node20": "^20.1.2", + "@ianvs/prettier-plugin-sort-imports": "^4.4.1", + "@playwright/test": "~1.49.1", + "@tsconfig/node20": "^20.1.4", "@types/body-scroll-lock": "^3.1.2", "@types/dom-to-image": "^2.6.7", - "@types/jsdom": "^21.1.6", - "@types/lodash": "^4.14.202", - "@types/node": "20.11.24", + "@types/jsdom": "^21.1.7", + "@types/lodash": "^4.17.14", + "@types/node": "^22.10.5", "@types/ua-parser-js": "^0.7.39", - "@vitejs/plugin-vue": "^4.6.2", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0", - "@vue/test-utils": "2.4.4", - "@vue/tsconfig": "^0.5.1", - "axe-playwright": "^2.0.1", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.22.0", - "eslint-plugin-vuejs-accessibility": "^2.2.1", - "jsdom": "^24.0.0", - "msw": "^2.2.2", - "postcss": "^8.4.35", - "prettier": "^3.2.5", - "prettier-plugin-css-order": "^2.0.1", - "prettier-plugin-jsdoc": "^1.3.0", - "sass": "^1.71.1", - "typescript": "^5.3.3", - "vite": "^5.1.4", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/eslint-config-prettier": "^10.1.0", + "@vue/eslint-config-typescript": "^14.2.0", + "@vue/test-utils": "2.4.6", + "@vue/tsconfig": "^0.7.0", + "axe-playwright": "^2.0.3", + "eslint": "^9.18.0", + "eslint-plugin-vue": "^9.32.0", + "eslint-plugin-vuejs-accessibility": "^2.4.1", + "jsdom": "^26.0.0", + "msw": "^2.7.0", + "postcss": "^8.4.49", + "prettier": "^3.4.2", + "prettier-plugin-css-order": "^2.1.2", + "prettier-plugin-jsdoc": "^1.3.2", + "sass": "^1.83.1", + "typescript": "^5.7.3", + "vite": "^6.0.7", "vite-svg-loader": "^5.1.0", - "vitest": "^1.3.1", - "vue-tsc": "^2.0.4" + "vitest": "^2.1.8", + "vue-tsc": "^2.2.0" }, "msw": { "workerDirectory": "public" diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js index 2d139dac5..fead0b3ff 100644 --- a/frontend/public/mockServiceWorker.js +++ b/frontend/public/mockServiceWorker.js @@ -8,8 +8,8 @@ * - Please do NOT serve this file on production. */ -const PACKAGE_VERSION = '2.2.13' -const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' +const PACKAGE_VERSION = '2.6.6' +const INTEGRITY_CHECKSUM = 'ca7800994cc8bfb5eb961e037c877074' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() @@ -62,7 +62,12 @@ self.addEventListener('message', async function (event) { sendToClient(client, { type: 'MOCKING_ENABLED', - payload: true, + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, }) break } @@ -155,6 +160,10 @@ async function handleRequest(event, requestId) { async function resolveMainClient(event) { const client = await self.clients.get(event.clientId) + if (activeClientIds.has(event.clientId)) { + return client + } + if (client?.frameType === 'top-level') { return client } @@ -183,12 +192,14 @@ async function getResponse(event, client, requestId) { const requestClone = request.clone() function passthrough() { - const headers = Object.fromEntries(requestClone.headers.entries()) - - // Remove internal MSW request header so the passthrough request - // complies with any potential CORS preflight checks on the server. - // Some servers forbid unknown request headers. - delete headers['x-msw-intention'] + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + headers.delete('accept', 'msw/passthrough') return fetch(requestClone, { headers }) } diff --git a/frontend/src/components/AppGallery.vue b/frontend/src/components/AppGallery.vue index 13c66a3ea..e86d5dc8e 100644 --- a/frontend/src/components/AppGallery.vue +++ b/frontend/src/components/AppGallery.vue @@ -53,7 +53,7 @@ $phone: 600px; grid-auto-rows: 1fr; place-content: center; // when content doesn't fill first row, limit width of gallery so that cell size is same as if first row was full - // e.g. on team page, so portraits in groups with only 1-2 members aren't bigger than portraits in groups with 3+ members + // e.g. team page, so portraits in groups with only 1-2 members aren't bigger than portraits in groups with 3+ members max-width: calc( (var(--cell)) * var(--content-cols) + (var(--content-cols) - 1) * var(--gap) ); diff --git a/frontend/src/components/AppHighlight.vue b/frontend/src/components/AppHighlight.vue index 502ccbda5..e74efdab3 100644 --- a/frontend/src/components/AppHighlight.vue +++ b/frontend/src/components/AppHighlight.vue @@ -12,9 +12,9 @@