diff --git a/.eslintrc.yaml b/.eslintrc.yaml index f90ec7d..b0e81e6 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -1,7 +1,7 @@ root: true extends: - eslint-config-mlauffer-nodejs - - plugin:jsdoc/recommended-typescript-error + - plugin:jsdoc/recommended-typescript - plugin:@typescript-eslint/recommended parser: '@typescript-eslint/parser' plugins: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index aaa05da..e52b770 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..fd0cd6f --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,25 @@ +name: NPM Package Publish + +on: + release: + types: [created] + +permissions: read-all + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + cache: npm + - run: npm ci --ignore-scripts + - run: npm run build + - run: npm publish --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..deacf95 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,20 @@ +name: release +on: + push: + branches: [master] + + +permissions: read-all + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: google-github-actions/release-please-action@v3 + with: + token: ${{ secrets.GH_PAT }} + release-type: node + package-name: vitest-environment-ui5 diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index 6ffc05b..3e1a473 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -21,7 +21,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d31d17..2f5f68e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,15 +10,18 @@ permissions: read-all jobs: test: runs-on: ubuntu-latest + permissions: + security-events: write steps: - - uses: actions/checkout@v3 - with: - persist-credentials: false + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 20 cache: npm - - run: npm install -g eslint --ignore-scripts - run: npm ci --ignore-scripts - - run: npm run lint - - run: npm test + - run: npm run build + - run: npm run test:ci + - run: npm run lint:ci + - uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: eslint.sarif diff --git a/.gitignore b/.gitignore index c6bba59..690a9ce 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +*.sarif diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26bacac..affb48d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,13 +37,17 @@ All submissions, including submissions by project members, require review. The p 1. Install dependencies: - npm install + npm ci -2. Lint the codebase: +2. Build: + + npm run build + +3. Lint the codebase: npm run lint -3. Run the tests: +4. Run the tests: npm test diff --git a/README.md b/README.md index 1ba0911..77e029d 100644 --- a/README.md +++ b/README.md @@ -1 +1,76 @@ -# vitest-environment-ui5 \ No newline at end of file +# vitest-environment-ui5 + +[![npm](https://img.shields.io/npm/v/vitest-environment-ui5)](https://www.npmjs.com/package/vitest-environment-ui5) [![test](https://github.com/mauriciolauffer/vitest-environment-ui5/actions/workflows/test.yml/badge.svg)](https://github.com/mauriciolauffer/vitest-environment-ui5/actions/workflows/test.yml) + +A `Vitest Environment` for unit testing `UI5` code. Run your unit tests in a blazing fast way! Neither webserver nor browser are required. It runs in `Node.js` and uses `jsdom` to emulate browser environment. + +See [Vitest Environment](https://vitest.dev/guide/environment.html) and [jsdom](https://github.com/jsdom/jsdom) for more details. + +## Installation + +Install `vitest-environment-ui5`, and `vitest`, as devDependencies: + +```shell +$ npm i -D vitest vitest-environment-ui5 +``` + +## Setup + +Vitest uses `node` as default test environment. In order to change it to a different environment, `vitest-environment-ui5`, we will either have to define it in `vitest.config.js` or add a `@vitest-environment` docblock at the top of the test file. See [Vitest Config](https://vitest.dev/config/#environment) for more details. + +`vitest-environment-ui5` builds `jsdom` from a local HTML file, no webserver required. The HTML file should contain the `UI5` bootstrap, similar to this one: [ui5 bootstrap](test/fixtures/ui5-unit-test.html). + +You can change the [UI5 bootstrap configuration](https://sapui5.hana.ondemand.com/sdk/#/topic/a04b0d10fb494d1cb722b9e341b584ba) as you wish, just like in your webapp. You can even open the file in a browser to see `UI5` being loaded. However, no tests will be executed. + +### Vitest Configuration + +```js +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "ui5", + environmentOptions: { + ui5: { + path: 'test/ui5-unit-test.html' // Path to the HTML file containing UI5 bootstrap + } + } + } +}); +``` + +### Vitest DocBlock + +```js +/** + * @vitest-environment ui5 + * @vitest-environment-options { "path": "test/ui5-unit-test.html" } + */ + +import {expect, test} from 'vitest'; + +test('UI5 is loaded', () => { + expect(window.sap).toBeTruthy(); + expect(sap).toBeTruthy(); + expect(sap.ui.getCore()).toBeTruthy(); + expect(sap.ui.version).toBeTruthy(); +}); +``` + +## Run + +Run the tests with `Vitest` [CLI](https://vitest.dev/guide/cli.html): + +```shell +$ vitest +``` + +## Author + +Mauricio Lauffer + +* LinkedIn: [https://www.linkedin.com/in/mauriciolauffer](https://www.linkedin.com/in/mauriciolauffer) + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/package-lock.json b/package-lock.json index 354d326..fd96651 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "@typescript-eslint/parser": "^6.7.2", "@vitest/coverage-v8": "^0.34.4", "eslint": "^8.49.0", - "eslint-config-mlauffer-nodejs": "^1.4.2" + "eslint-config-mlauffer-nodejs": "^1.4.4", + "eslint-plugin-vitest": "^0.3.1" }, "peerDependencies": { "vitest": "^0.34.4" @@ -753,6 +754,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/eslint-formatter-sarif": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/eslint-formatter-sarif/-/eslint-formatter-sarif-3.0.0.tgz", + "integrity": "sha512-KIKkT44hEqCzqxODYwFMUvYEK0CrdHx/Ll9xiOWgFbBSRuzbxmVy4d/tzfgoucGz72HJZNOMjuyzFTBKntRK5Q==", + "dev": true, + "dependencies": { + "eslint": "^8.9.0", + "jschardet": "latest", + "lodash": "^4.17.14", + "utf8": "^3.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1874,14 +1890,15 @@ } }, "node_modules/eslint-config-mlauffer-nodejs": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/eslint-config-mlauffer-nodejs/-/eslint-config-mlauffer-nodejs-1.4.2.tgz", - "integrity": "sha512-POQKKHyBHa4iXZQsmLFvSqWp8sXtZZoZELLMmI8s8cWDKThWaKSQ5itb2+DGm4NdvludZ+uFpA97rO+rblmlPA==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/eslint-config-mlauffer-nodejs/-/eslint-config-mlauffer-nodejs-1.4.4.tgz", + "integrity": "sha512-g5pS3DG8s3+vBkQSVFz97Fbk54Fuk1qDiG9qGGKZFW/GbFy9w7A9B9qpZZ6P2k6jjF4O9Uln5M71tuToR9OyPA==", "dev": true, "dependencies": { + "@microsoft/eslint-formatter-sarif": "^3.0.0", "eslint-config-google": "^0.14.0", "eslint-plugin-anti-trojan-source": "^1.1.1", - "eslint-plugin-jsdoc": "^46.5.1", + "eslint-plugin-jsdoc": "^46.8.2", "eslint-plugin-security": "^1.7.1", "eslint-plugin-sonarjs": "^0.21.0" }, @@ -1902,9 +1919,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.8.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.1.tgz", - "integrity": "sha512-uTce7IBluPKXIQMWJkIwFsI1gv7sZRmLjctca2K5DIxPi8fSBj9f4iru42XmGwuiMyH2f3nfc60sFmnSGv4Z/A==", + "version": "46.8.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.2.tgz", + "integrity": "sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==", "dev": true, "dependencies": { "@es-joy/jsdoccomment": "~0.40.1", @@ -1945,6 +1962,31 @@ "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/eslint-plugin-vitest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.1.tgz", + "integrity": "sha512-GeR3zISHmqUGWK2sfW+eyCZivMqiQYzPf9UttHXBiEyMveS/jkKLHCrHUllwr3Hz1+i0zoseANd2xL0cFha8Eg==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^6.5.0", + "typescript": "^5.2.2" + }, + "engines": { + "node": "14.x || >= 16" + }, + "peerDependencies": { + "eslint": ">=8.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "vitest": { + "vitest": "*" + } + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -2228,9 +2270,9 @@ } }, "node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2585,6 +2627,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jschardet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.0.0.tgz", + "integrity": "sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", @@ -2729,6 +2780,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3779,7 +3836,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3820,6 +3876,12 @@ "requires-port": "^1.0.0" } }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", diff --git a/package.json b/package.json index 03e5fc2..2561e7a 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,13 @@ "build": "tsc", "watch": "tsc -w", "lint": "eslint src test --cache", + "lint:ci": "eslint src -f @microsoft/eslint-formatter-sarif -o eslint.sarif", "test": "cd test/env-ui5 && vitest", "test:ci": "cd test/env-ui5 && vitest run --coverage" }, "peerDependencies": { - "vitest": "^0.34.4" + "vitest": ">=0.30", + "typescript": ">=5" }, "repository": { "type": "git", @@ -49,6 +51,7 @@ "@typescript-eslint/parser": "^6.7.2", "@vitest/coverage-v8": "^0.34.4", "eslint": "^8.49.0", - "eslint-config-mlauffer-nodejs": "^1.4.2" + "eslint-config-mlauffer-nodejs": "^1.4.4", + "eslint-plugin-vitest": "^0.3.1" } } diff --git a/src/index.ts b/src/index.ts index 9a140ca..1e4a08c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,13 @@ import type {Environment} from 'vitest'; import {populateGlobal} from 'vitest/environments'; -import type {JSDOM, DOMWindow, FileOptions} from 'jsdom'; -import type Ui5Options from '../types/globals'; +import {JSDOM} from 'jsdom'; +import type {DOMWindow, FileOptions} from 'jsdom'; /** - * Build JSDOM from HTML file containing UI5 bootstrap configuration + * Get jsdom configuration to build JSDOM from HTML file containing UI5 bootstrap configuration */ -async function buildFromFile(JSDOM: any, ui5: Ui5Options): Promise { - const options: FileOptions = { +function getConfiguration(): FileOptions { + return { resources: 'usable', referrer: 'https://ui5.sap.com/', runScripts: 'dangerously', @@ -17,7 +17,7 @@ async function buildFromFile(JSDOM: any, ui5: Ui5Options): Promise { Object.defineProperty(jsdomWindow, 'matchMedia', { writable: true, configurable: true, - value: (query: any) => ({ + value: (query: string) => ({ matches: false, media: query, onchange: null, @@ -37,8 +37,6 @@ async function buildFromFile(JSDOM: any, ui5: Ui5Options): Promise { }); } }; - - return JSDOM.fromFile(ui5.path, options); } @@ -62,11 +60,11 @@ function addScriptEvents(window: DOMWindow): void { /** * Await UI5 to be loaded: onInit event */ -async function ui5Loaded(window: DOMWindow): Promise { +async function ui5Ready(window: DOMWindow): Promise { return new Promise((resolve, reject) => { window.onUi5ModulesLoaded = (isLoaded: boolean) => { setTimeout(() => { - isLoaded ? resolve(true) : reject(false); + isLoaded ? resolve(true) : reject(new Error('sap-ui-bootstrap')); }, 0); }; }); @@ -79,10 +77,10 @@ export default ({ name: 'ui5', transformMode: 'web', async setup(global, {ui5 = {}}) { - const {JSDOM} = await import('jsdom'); - const dom = await buildFromFile(JSDOM, ui5); + // const {JSDOM} = await import('jsdom'); + const dom = await JSDOM.fromFile(ui5.path, getConfiguration()); addScriptEvents(dom.window); - await ui5Loaded(dom.window); + await ui5Ready(dom.window); const hrefFile = dom.window.location.href; dom.reconfigure({url: 'http://localhost/'}); // Workaround to avoid > SecurityError: localStorage is not available for opaque origins const {keys, originals} = populateGlobal(global, dom.window, {bindFunctions: true}); diff --git a/test/.eslintrc.yaml b/test/.eslintrc.yaml new file mode 100644 index 0000000..60bf8ff --- /dev/null +++ b/test/.eslintrc.yaml @@ -0,0 +1,8 @@ +root: false +extends: + - plugin:vitest/recommended +globals: + sap: true + window: true +rules: + jsdoc/check-tag-names: off diff --git a/test/env-ui5-with-docblock/base.test.ts b/test/env-ui5-with-docblock/base.test.ts index cb50752..65f2e32 100644 --- a/test/env-ui5-with-docblock/base.test.ts +++ b/test/env-ui5-with-docblock/base.test.ts @@ -1,13 +1,13 @@ /** * @vitest-environment ui5 - * @vitest-environment-options { "path": "ui5.html" } + * @vitest-environment-options { "path": "../fixtures/ui5.html" } */ import {expect, test} from 'vitest'; test('ui5 env is defined', () => { expect(expect.getState().environment).toBe('ui5'); - expect(globalThis?.__vitest_worker__?.ctx?.environment?.options).toMatchObject({ui5: {path: 'ui5.html'}}); + expect(globalThis?.__vitest_worker__?.ctx?.environment?.options).toMatchObject({ui5: {path: '../fixtures/ui5.html'}}); }); test('jsdom is initiated', () => { @@ -18,4 +18,6 @@ test('jsdom is initiated', () => { test('ui5 is loaded', () => { expect(window.sap).toBeTruthy(); expect(sap).toBeTruthy(); + expect(sap.ui.getCore()).toBeTruthy(); + expect(sap.ui.version).toBeTruthy(); }); diff --git a/test/env-ui5/base.test.ts b/test/env-ui5/base.test.ts index cb50752..9bb6fd3 100644 --- a/test/env-ui5/base.test.ts +++ b/test/env-ui5/base.test.ts @@ -1,13 +1,8 @@ -/** - * @vitest-environment ui5 - * @vitest-environment-options { "path": "ui5.html" } - */ - import {expect, test} from 'vitest'; test('ui5 env is defined', () => { expect(expect.getState().environment).toBe('ui5'); - expect(globalThis?.__vitest_worker__?.ctx?.environment?.options).toMatchObject({ui5: {path: 'ui5.html'}}); + expect(globalThis.__vitest_worker__.ctx.config.environmentOptions).toMatchObject({ui5: {path: '../fixtures/ui5.html'}}); }); test('jsdom is initiated', () => { @@ -18,4 +13,6 @@ test('jsdom is initiated', () => { test('ui5 is loaded', () => { expect(window.sap).toBeTruthy(); expect(sap).toBeTruthy(); + expect(sap.ui.getCore()).toBeTruthy(); + expect(sap.ui.version).toBeTruthy(); }); diff --git a/test/env-ui5/ui5.html b/test/env-ui5/ui5.html deleted file mode 100644 index a8016dd..0000000 --- a/test/env-ui5/ui5.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - OpenUI5 Todo App - - - - - - - - diff --git a/test/env-ui5/vitest.config.ts b/test/env-ui5/vitest.config.ts new file mode 100644 index 0000000..153bc62 --- /dev/null +++ b/test/env-ui5/vitest.config.ts @@ -0,0 +1,12 @@ +import {defineConfig} from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'ui5', + environmentOptions: { + ui5: { + path: '../fixtures/ui5.html' + } + } + } +}); diff --git a/test/env-ui5-with-docblock/ui5.html b/test/fixtures/ui5.html similarity index 100% rename from test/env-ui5-with-docblock/ui5.html rename to test/fixtures/ui5.html