diff --git a/.circleci/config.yml b/.circleci/config.yml index 217678d71..8aaa9b41a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,21 @@ +# This is the based configuration required by CircleCI to run a build. +# +# The repository uses the dynamic configuration to generate +# tasks for executing tests and checking the code coverage. +# +# This configuration aims to prepare a complete design and continue checking +# the repository in a new workflow. +# +# To modify the commands to execute on CI, review the following files: +# - scripts/ci/generate-circleci-configuration.js - the script that creates the `config-tests.yml` file used on the new workflow. +# - .circleci/template.yml - the template filled with data to execute. +# +# Useful resources: +# - https://circleci.com/docs/using-dynamic-configuration/ version: 2.1 +setup: true + parameters: triggerCommitHash: type: string @@ -11,16 +27,10 @@ parameters: type: boolean default: false -commands: - bootstrap_repository_command: - description: "Bootstrap the repository" - steps: - - install_ssh_keys_command - - run: - name: Install dependencies - command: yarn install - - prepare_environment_variables_commands +orbs: + continuation: circleci/continuation@0.1.2 +commands: install_ssh_keys_command: description: "Install SSH keys" steps: @@ -28,242 +38,23 @@ commands: fingerprints: - "a0:41:a2:56:c8:7d:3f:29:41:d1:87:92:fd:50:2b:6b" - npm_login_command: - description: "Enable interacting with `npm` using an auth token" - steps: - - run: - name: Login to the npm registry using '.npmrc' file - command: echo "//registry.npmjs.org/:_authToken=\${CKE5_NPM_TOKEN}" > ~/.npmrc - - git_credentials_command: - description: "Setup git configuration" - steps: - - run: - name: Setup git configuration - command: | - git config --global user.email "ckeditor-bot@cksource.com" - git config --global user.name "CKEditorBot" - - prepare_environment_variables_commands: - description: "Prepare non-secret environment variables" - steps: - - run: - name: Prepare environment variables - command: | - #!/bin/bash - - # Non-secret environment variables needed for the pipeline scripts. - CKE5_GITHUB_ORGANIZATION="ckeditor" - CKE5_GITHUB_REPOSITORY="ckeditor5-dev" - CKE5_CIRCLE_APPROVAL_JOB_NAME="release_approval" - CKE5_GITHUB_RELEASE_BRANCH="master" - - echo export CKE5_CIRCLE_APPROVAL_JOB_NAME=$CKE5_CIRCLE_APPROVAL_JOB_NAME >> $BASH_ENV - echo export CKE5_GITHUB_RELEASE_BRANCH=$CKE5_GITHUB_RELEASE_BRANCH >> $BASH_ENV - echo export CKE5_GITHUB_ORGANIZATION=$CKE5_GITHUB_ORGANIZATION >> $BASH_ENV - echo export CKE5_GITHUB_REPOSITORY=$CKE5_GITHUB_REPOSITORY >> $BASH_ENV - echo export CKE5_GITHUB_REPOSITORY_SLUG="$CKE5_GITHUB_ORGANIZATION/$CKE5_GITHUB_REPOSITORY" >> $BASH_ENV - echo export CKE5_COMMIT_SHA1=$CIRCLE_SHA1 >> $BASH_ENV - jobs: - notify_ci_failure: - machine: true - parameters: - hideAuthor: - type: string - default: "false" - steps: - - checkout - - bootstrap_repository_command - - run: - # In the PRs that comes from forked repositories, we do not share secret variables. - # Hence, some of the scripts will not be executed. - name: 👤 Verify if the build was triggered by community - Check if the build should continue - command: | - #!/bin/bash - - if [[ -z ${COVERALLS_REPO_TOKEN} ]]; - then - circleci-agent step halt - fi - - run: - environment: - CKE5_SLACK_NOTIFY_HIDE_AUTHOR: << parameters.hideAuthor >> - CKE5_PIPELINE_NUMBER: << pipeline.number >> - name: Waiting for other jobs to finish and sending notification on failure - command: yarn ckeditor5-dev-ci-circle-workflow-notifier - no_output_timeout: 1h - - validate_and_tests: - machine: true - resource_class: large - steps: - - checkout - - bootstrap_repository_command - - run: - name: Execute ESLint - command: yarn run lint - - run: - name: Run unit tests - command: yarn run coverage - - unless: - # Upload the code coverage results for non-nightly builds only. - condition: << pipeline.parameters.isNightly >> - steps: - - run: - # In the PRs that comes from forked repositories, we do not share secret variables. - # Hence, some of the scripts will not be executed. - name: 👤 Verify if the build was triggered by community - Check if the build should continue - command: | - #!/bin/bash - - if [[ -z ${COVERALLS_REPO_TOKEN} ]]; - then - circleci-agent step halt - fi - - run: - name: Upload code coverage - command: cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js - - release_prepare: - machine: true - resource_class: large - steps: - - checkout - - bootstrap_repository_command - - run: - name: Check if packages are ready to be released - command: yarn run release:prepare-packages --verbose --compile-only - - trigger_release_process: - machine: true - resource_class: large - steps: - - checkout - - bootstrap_repository_command - - run: - name: Verify if the project is ready to release - command: | - #!/bin/bash - - # Do not fail if the Node script ends with non-zero exit code. - set +e - - node scripts/ci/is-project-ready-to-release.js - EXIT_CODE=$( echo $? ) - - if [ ${EXIT_CODE} -eq 1 ]; - then - circleci-agent step halt - fi - - run: - name: Trigger the release pipeline - command: yarn ckeditor5-dev-ci-trigger-circle-build - - release_project: + generate_configuration: machine: true - resource_class: large steps: - checkout - - bootstrap_repository_command - - run: - name: Verify the trigger commit from the repository - command: | - #!/bin/bash - - CKE5_LATEST_COMMIT_HASH=$( git log -n 1 --pretty=format:%H origin/master ) - CKE5_TRIGGER_COMMIT_HASH=<< pipeline.parameters.triggerCommitHash >> - - if [[ "${CKE5_LATEST_COMMIT_HASH}" != "${CKE5_TRIGGER_COMMIT_HASH}" ]]; then - echo "There is a newer commit in the repository on the \`#master\` branch. Use its build to start the release." - circleci-agent step halt - fi - - npm_login_command - - git_credentials_command - - run: - name: Verify if a releaser triggered the job - command: | - #!/bin/bash - - # Do not fail if the Node script ends with non-zero exit code. - set +e - - yarn ckeditor5-dev-ci-is-job-triggered-by-member - EXIT_CODE=$( echo $? ) - - if [ ${EXIT_CODE} -ne 0 ]; - then - echo "Aborting the release due to failed verification of the approver (no rights to release)." - circleci-agent step halt - fi - - run: - name: Disable the redundant workflows option - command: yarn ckeditor5-dev-ci-circle-disable-auto-cancel-builds - - run: - name: Prepare the new version to release - command: npm run release:prepare-packages -- --verbose - - run: - name: Publish the packages - command: npm run release:publish-packages -- --verbose + - install_ssh_keys_command - run: - name: Enable the redundant workflows option - command: yarn ckeditor5-dev-ci-circle-enable-auto-cancel-builds - when: always + name: Install dependencies + command: yarn install - run: - name: Pack the "release/" directory (in case of failure) - command: | - zip -r ./release.zip ./release - when: always - - store_artifacts: - path: ./release.zip - when: always + name: Generate a new configuration to check all packages in the repository + command: node scripts/ci/generate-circleci-configuration.js + - continuation/continue: + configuration_path: .circleci/config-tests.yml workflows: version: 2 - main: - when: - and: - - equal: [ false, << pipeline.parameters.isNightly >> ] - - equal: [ false, << pipeline.parameters.isRelease >> ] - jobs: - - validate_and_tests - - release_prepare - - trigger_release_process: - requires: - - validate_and_tests - - release_prepare - filters: - branches: - only: - - master - - notify_ci_failure: - filters: - branches: - only: - - master - - release: - when: - and: - - equal: [ false, << pipeline.parameters.isNightly >> ] - - equal: [ true, << pipeline.parameters.isRelease >> ] - jobs: - - release_approval: - type: approval - - release_project: - requires: - - release_approval - - nightly: - when: - and: - - equal: [ true, << pipeline.parameters.isNightly >> ] - - equal: [ false, << pipeline.parameters.isRelease >> ] + config: jobs: - - validate_and_tests - - notify_ci_failure: - hideAuthor: "true" - filters: - branches: - only: - - master + - generate_configuration diff --git a/.circleci/template.yml b/.circleci/template.yml new file mode 100644 index 000000000..5b153d1b6 --- /dev/null +++ b/.circleci/template.yml @@ -0,0 +1,269 @@ +version: 2.1 + +parameters: + triggerCommitHash: + type: string + default: "" + isNightly: + type: boolean + default: false + isRelease: + type: boolean + default: false + +commands: + bootstrap_repository_command: + description: "Bootstrap the repository" + steps: + - install_ssh_keys_command + - run: + name: Install dependencies + command: yarn install + - prepare_environment_variables_commands + + install_ssh_keys_command: + description: "Install SSH keys" + steps: + - add_ssh_keys: + fingerprints: + - "a0:41:a2:56:c8:7d:3f:29:41:d1:87:92:fd:50:2b:6b" + + npm_login_command: + description: "Enable interacting with `npm` using an auth token" + steps: + - run: + name: Login to the npm registry using '.npmrc' file + command: echo "//registry.npmjs.org/:_authToken=\${CKE5_NPM_TOKEN}" > ~/.npmrc + + git_credentials_command: + description: "Setup git configuration" + steps: + - run: + name: Setup git configuration + command: | + git config --global user.email "ckeditor-bot@cksource.com" + git config --global user.name "CKEditorBot" + + prepare_environment_variables_commands: + description: "Prepare non-secret environment variables" + steps: + - run: + name: Prepare environment variables + command: | + #!/bin/bash + + # Non-secret environment variables needed for the pipeline scripts. + CKE5_GITHUB_ORGANIZATION="ckeditor" + CKE5_GITHUB_REPOSITORY="ckeditor5-dev" + CKE5_CIRCLE_APPROVAL_JOB_NAME="release_approval" + CKE5_GITHUB_RELEASE_BRANCH="master" + + echo export CKE5_CIRCLE_APPROVAL_JOB_NAME=$CKE5_CIRCLE_APPROVAL_JOB_NAME >> $BASH_ENV + echo export CKE5_GITHUB_RELEASE_BRANCH=$CKE5_GITHUB_RELEASE_BRANCH >> $BASH_ENV + echo export CKE5_GITHUB_ORGANIZATION=$CKE5_GITHUB_ORGANIZATION >> $BASH_ENV + echo export CKE5_GITHUB_REPOSITORY=$CKE5_GITHUB_REPOSITORY >> $BASH_ENV + echo export CKE5_GITHUB_REPOSITORY_SLUG="$CKE5_GITHUB_ORGANIZATION/$CKE5_GITHUB_REPOSITORY" >> $BASH_ENV + echo export CKE5_COMMIT_SHA1=$CIRCLE_SHA1 >> $BASH_ENV + +jobs: + notify_ci_failure: + machine: true + parameters: + hideAuthor: + type: string + default: "false" + steps: + - checkout + - bootstrap_repository_command + - run: + # In the PRs that comes from forked repositories, we do not share secret variables. + # Hence, some of the scripts will not be executed. + name: 👤 Verify if the build was triggered by community - Check if the build should continue + command: | + #!/bin/bash + + if [[ -z ${COVERALLS_REPO_TOKEN} ]]; + then + circleci-agent step halt + fi + - run: + environment: + CKE5_SLACK_NOTIFY_HIDE_AUTHOR: << parameters.hideAuthor >> + CKE5_PIPELINE_NUMBER: << pipeline.number >> + name: Waiting for other jobs to finish and sending notification on failure + command: yarn ckeditor5-dev-ci-circle-workflow-notifier + no_output_timeout: 1h + + validate_and_tests: + machine: true + resource_class: large + steps: + - checkout + - bootstrap_repository_command + - run: + name: Execute ESLint + command: yarn run lint + - unless: + # Upload the code coverage results for non-nightly builds only. + condition: << pipeline.parameters.isNightly >> + steps: + - run: + # In the PRs that comes from forked repositories, we do not share secret variables. + # Hence, some of the scripts will not be executed. + name: 👤 Verify if the build was triggered by community - Check if the build should continue + command: | + #!/bin/bash + + if [[ -z ${COVERALLS_REPO_TOKEN} ]]; + then + circleci-agent step halt + fi + - run: + name: Install the "coveralls" package + command: yarn add --ignore-workspace-root-check coveralls + - run: + name: Upload code coverage + command: cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js + + release_prepare: + machine: true + resource_class: large + steps: + - checkout + - bootstrap_repository_command + - run: + name: Check if packages are ready to be released + command: yarn run release:prepare-packages --verbose --compile-only + + trigger_release_process: + machine: true + resource_class: large + steps: + - checkout + - bootstrap_repository_command + - run: + name: Verify if the project is ready to release + command: | + #!/bin/bash + + # Do not fail if the Node script ends with non-zero exit code. + set +e + + node scripts/ci/is-project-ready-to-release.js + EXIT_CODE=$( echo $? ) + + if [ ${EXIT_CODE} -eq 1 ]; + then + circleci-agent step halt + fi + - run: + name: Trigger the release pipeline + command: yarn ckeditor5-dev-ci-trigger-circle-build + + release_project: + machine: true + resource_class: large + steps: + - checkout + - bootstrap_repository_command + - run: + name: Verify the trigger commit from the repository + command: | + #!/bin/bash + + CKE5_LATEST_COMMIT_HASH=$( git log -n 1 --pretty=format:%H origin/master ) + CKE5_TRIGGER_COMMIT_HASH=<< pipeline.parameters.triggerCommitHash >> + + if [[ "${CKE5_LATEST_COMMIT_HASH}" != "${CKE5_TRIGGER_COMMIT_HASH}" ]]; then + echo "There is a newer commit in the repository on the \`#master\` branch. Use its build to start the release." + circleci-agent step halt + fi + - npm_login_command + - git_credentials_command + - run: + name: Verify if a releaser triggered the job + command: | + #!/bin/bash + + # Do not fail if the Node script ends with non-zero exit code. + set +e + + yarn ckeditor5-dev-ci-is-job-triggered-by-member + EXIT_CODE=$( echo $? ) + + if [ ${EXIT_CODE} -ne 0 ]; + then + echo "Aborting the release due to failed verification of the approver (no rights to release)." + circleci-agent step halt + fi + - run: + name: Disable the redundant workflows option + command: yarn ckeditor5-dev-ci-circle-disable-auto-cancel-builds + - run: + name: Prepare the new version to release + command: npm run release:prepare-packages -- --verbose + - run: + name: Publish the packages + command: npm run release:publish-packages -- --verbose + - run: + name: Enable the redundant workflows option + command: yarn ckeditor5-dev-ci-circle-enable-auto-cancel-builds + when: always + - run: + name: Pack the "release/" directory (in case of failure) + command: | + zip -r ./release.zip ./release + when: always + - store_artifacts: + path: ./release.zip + when: always + +workflows: + version: 2 + main: + when: + and: + - equal: [ false, << pipeline.parameters.isNightly >> ] + - equal: [ false, << pipeline.parameters.isRelease >> ] + jobs: + - validate_and_tests + - release_prepare + - trigger_release_process: + requires: + - validate_and_tests + - release_prepare + filters: + branches: + only: + - master + - notify_ci_failure: + filters: + branches: + only: + - master + + release: + when: + and: + - equal: [ false, << pipeline.parameters.isNightly >> ] + - equal: [ true, << pipeline.parameters.isRelease >> ] + jobs: + - release_approval: + type: approval + - release_project: + requires: + - release_approval + + nightly: + when: + and: + - equal: [ true, << pipeline.parameters.isNightly >> ] + - equal: [ false, << pipeline.parameters.isRelease >> ] + jobs: + - validate_and_tests + - notify_ci_failure: + hideAuthor: "true" + filters: + branches: + only: + - master diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 55% rename from .eslintrc.js rename to .eslintrc.cjs index 22527e8f2..f8cf0f0a8 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -7,17 +7,26 @@ module.exports = { extends: 'ckeditor5', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, env: { node: true }, ignorePatterns: [ '**/dist/*', '**/coverage/**', - '**/node_modules/**' + '**/node_modules/**', + + // ESLint does not understand `import ... with { ... }`. + // See: https://github.com/eslint/eslint/discussions/15305. + 'packages/ckeditor5-dev-ci/lib/data/index.js', + 'packages/ckeditor5-dev-transifex/lib/data/index.js' ], rules: { 'no-console': 'off', - 'ckeditor5-rules/require-file-extensions-in-imports': 'off', + 'mocha/no-global-tests': 'off', 'ckeditor5-rules/license-header': [ 'error', { headerLines: [ '/**', @@ -29,9 +38,11 @@ module.exports = { }, overrides: [ { - files: [ './packages/ckeditor5-dev-build-tools/tests/**/*' ], + files: [ + './packages/typedoc-plugins/**/*' + ], rules: { - 'mocha/no-global-tests': 'off' + 'ckeditor5-rules/require-file-extensions-in-imports': 'off', } } ] diff --git a/.gitignore b/.gitignore index 6050712d8..faaf9bd36 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,9 @@ executeinparallel-integration.log # Compiled TS. packages/ckeditor5-dev-build-tools/dist + +packages/ckeditor5-dev-release-tools/tests/test-fixtures/** +!packages/ckeditor5-dev-release-tools/tests/test-fixtures/.gitkeep + +# Generated automatically via CircleCI. +.circleci/config-tests.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a4cbe49..7902e5f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,11 @@ Changelog ========= -## [43.0.0](https://github.com/ckeditor/ckeditor5-dev/compare/v42.1.0...v43.0.0) (2024-09-09) - -### MAJOR BREAKING CHANGES [ℹ️](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html#major-and-minor-breaking-changes) - -* **[utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils)**: The `git` and `workspace` objects are no longer exported from the package. Also, the following functions are no longer available in the `tools` object: - -* `isDirectory()` -* `isFile()` -* `isSymlink()` -* `sortObject()` -* `readPackageName()` -* `npmInstall()` -* `npmUninstall()` -* `npmUpdate()` -* `copyTemplateFile()` -* `copyFile()` -* `getGitUrlFromNpm()` -* `removeSymlink()` -* `clean()` +## [44.0.0-alpha.5](https://github.com/ckeditor/ckeditor5-dev/compare/v44.0.0-alpha.4...v44.0.0-alpha.5) (2024-09-24) ### Other changes -* **[dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker)**: The dependency checker analyzes dependencies by including the `lib/` and `bin/` directories as production code. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/e84c7019a61fa31c233e961afed014c1c9303989)) -* **[utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils)**: Removed several utilities functions non-used in the CKEditor 5 environment. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/e84c7019a61fa31c233e961afed014c1c9303989)) -* Added several missing `dependencies` and `devDependencies` in packages. Also, removed non-used ones. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/e84c7019a61fa31c233e961afed014c1c9303989)) +* **[web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler)**: Restored the previous version of the "puppeteer" package as the latest version is not too stable. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/3c2df981b960e20d4de943f527375dc408a425d7)) ### Released packages @@ -36,48 +16,28 @@ Check out the [Versioning policy](https://ckeditor.com/docs/ckeditor5/latest/fra Other releases: -* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/jsdoc-plugins](https://www.npmjs.com/package/@ckeditor/jsdoc-plugins/v/43.0.0): v42.1.0 => v43.0.0 -* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/43.0.0): v42.1.0 => v43.0.0 +* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 +* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/44.0.0-alpha.5): v44.0.0-alpha.4 => v44.0.0-alpha.5 -## [43.0.0-alpha.0](https://github.com/ckeditor/ckeditor5-dev/compare/v42.1.0...v43.0.0-alpha.0) (2024-09-02) +## [44.0.0-alpha.4](https://github.com/ckeditor/ckeditor5-dev/compare/v44.0.0-alpha.3...v44.0.0-alpha.4) (2024-09-24) -### MAJOR BREAKING CHANGES [ℹ️](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html#major-and-minor-breaking-changes) - -* **[utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils)**: The `git` and `workspace` objects are no longer exported from the package. Also, the following functions are no longer available in the `tools` object: - -* `isDirectory()` -* `isFile()` -* `isSymlink()` -* `sortObject()` -* `readPackageName()` -* `npmInstall()` -* `npmUninstall()` -* `npmUpdate()` -* `copyTemplateFile()` -* `copyFile()` -* `getGitUrlFromNpm()` -* `removeSymlink()` -* `clean()` - -### Other changes +### Bug fixes -* **[dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker)**: The dependency checker analyzes dependencies by including the `lib/` and `bin/` directories as production code. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/e84c7019a61fa31c233e961afed014c1c9303989)) -* **[utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils)**: Removed several utilities functions non-used in the CKEditor 5 environment. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/e84c7019a61fa31c233e961afed014c1c9303989)) -* Added several missing `dependencies` and `devDependencies` in packages. Also, removed non-used ones. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/e84c7019a61fa31c233e961afed014c1c9303989)) +* **[tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests)**: Prevent crashing the manual test server when reading a non-existing file due to an "ERR_HTTP_HEADERS_SENT" Node.js error. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/15a8f79e15a69ad4cec8365cb5d86cd731ba1953)) +* **[web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler)**: Use `jsonValue()` method to get the serialized arguments instead of calling `evaluate()` method, which may cause unhandled rejection due to destroyed context. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/c89444e9598bffbd7b3d1070df607ac05d54c2d9)) ### Released packages @@ -88,32 +48,28 @@ Check out the [Versioning policy](https://ckeditor.com/docs/ckeditor5/latest/fra Other releases: -* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/jsdoc-plugins](https://www.npmjs.com/package/@ckeditor/jsdoc-plugins/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 -* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/43.0.0-alpha.0): v42.1.0 => v43.0.0-alpha.0 +* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 +* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/44.0.0-alpha.4): v44.0.0-alpha.3 => v44.0.0-alpha.4 -## [42.1.0](https://github.com/ckeditor/ckeditor5-dev/compare/v42.0.1...v42.1.0) (2024-08-29) - -### Features - -* **[build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools)**: Introduced a new `loadSourcemaps` plugin for loading source maps of external dependencies. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/defb966ca3e090d062d173e5098a2325696491ec)) +## [44.0.0-alpha.3](https://github.com/ckeditor/ckeditor5-dev/compare/v44.0.0-alpha.2...v44.0.0-alpha.3) (2024-09-23) -### Bug fixes +### Other changes -* **[build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools)**: Fixed source maps generation for the UMD build. Fixes [ckeditor/ckeditor5#16984](https://github.com/ckeditor/ckeditor5/issues/16984). ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/defb966ca3e090d062d173e5098a2325696491ec)) +* **[tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests)**: Downgrade the "sinon" package as it is not compatible with current CKEditor 5 tests. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/e94009abd804176cc381b9bac3de42b1da0db3da)) +* **[web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler)**: Aligned internals to the latest Puppeteer API. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/d70d99ffeed4609de954bae936e6f01875ded5a8)) ### Released packages @@ -122,37 +78,31 @@ Check out the [Versioning policy](https://ckeditor.com/docs/ckeditor5/latest/fra
Released packages (summary) -Releases containing new features: - -* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/42.1.0): v42.0.1 => v42.1.0 - Other releases: -* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/jsdoc-plugins](https://www.npmjs.com/package/@ckeditor/jsdoc-plugins/v/42.1.0): v42.0.1 => v42.1.0 -* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/42.1.0): v42.0.1 => v42.1.0 +* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3 +* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/44.0.0-alpha.3): v44.0.0-alpha.2 => v44.0.0-alpha.3
-## [42.0.1](https://github.com/ckeditor/ckeditor5-dev/compare/v42.0.0...v42.0.1) (2024-08-13) - -### Bug fixes - -* **[tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests)**: Added a Chrome flag to prevent displaying the search engine choice screen that disrupts automated tests in windowed mode. Closes [ckeditor/ckeditor5#16825](https://github.com/ckeditor/ckeditor5/issues/16825). ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/4f7291f1f8114ed0184f11a51c74752c6d8ecaa9)) +## [44.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-dev/compare/v44.0.0-alpha.1...v44.0.0-alpha.2) (2024-09-23) ### Other changes -* **[stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot)**: Aligned stale bot to recent changes in the GitHub GraphQL API in the `repository.labels` connection. GitHub recently started returning a lot of mismatched labels for the query and now stale bot ensures that only the required ones are used. Closes [ckeditor/ckeditor5#16872](https://github.com/ckeditor/ckeditor5/issues/16872). ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/666daf6cfe52b5ce63e7937168022eb86fcb4f9c)) +* **[docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs)**: Support for passing an array of files to ignore when preparing API. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/3c858289a5826c9fceddfb380a4e35d48b44a099)) +* **[tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests)**: Restored the previous version of Chai and sinon-chai packages due to issues with processing ESM in Karma. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/833ac929e4193b6791c79cdc5df05e67539c0c7f)) +* Use the "2021" edition as a default preset for CKEditor 5 files (`postcss-nesting`). ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/ce3902d5855f1b3ba886dea1db195a5b27e22026)) ### Released packages @@ -163,46 +113,31 @@ Check out the [Versioning policy](https://ckeditor.com/docs/ckeditor5/latest/fra Other releases: -* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/jsdoc-plugins](https://www.npmjs.com/package/@ckeditor/jsdoc-plugins/v/42.0.1): v42.0.0 => v42.0.1 -* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/42.0.1): v42.0.0 => v42.0.1 +* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 +* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/44.0.0-alpha.2): v44.0.0-alpha.1 => v44.0.0-alpha.2 -## [42.0.0](https://github.com/ckeditor/ckeditor5-dev/compare/v41.0.0...v42.0.0) (2024-07-29) - -We are excited to announce a new major release of the `@ckeditor/ckeditor5-dev-*` packages. - -### Release highlights - -This release brings the updated configuration for the build tools. As it might produce output incompatible with the previous settings, this release is marked as a major bump. - -The [`@ckeditor/ckeditor5-dev-build-tools`](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools) package now supports a new `globals` option, which allows passing pairs of external package names and associated global variables used in the `umd` build. - -Additionally, the global names for the `ckeditor5` and `ckeditor5-premium-features` packages in the UMD builds have been changed to `CKEDITOR` and `CKEDITOR_PREMIUM_FEATURES` respectively. - -### MAJOR BREAKING CHANGES [ℹ️](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html#major-and-minor-breaking-changes) +## [44.0.0-alpha.1](https://github.com/ckeditor/ckeditor5-dev/compare/v44.0.0-alpha.0...v44.0.0-alpha.1) (2024-09-23) -* **[build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools)**: The global names for the `ckeditor5` and `ckeditor5-premium-features` packages in the UMD builds have been changed to `CKEDITOR` and `CKEDITOR_PREMIUM_FEATURES` respectively. - -### MINOR BREAKING CHANGES [ℹ️](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html#major-and-minor-breaking-changes) +### Features -* **[build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools)**: Ability to pass `globals` parameter if necessary for external imports in `umd` bundles. +* **[release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools)**: Allow defining a main branch when generating the changelog entries. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/8b5078e67ebbbe9e8a5a952fa18646dfca6a2563)) -### Bug fixes +### Other changes -* **[build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools)**: Ability to pass `globals` parameter if necessary for external imports in `umd` bundles. See https://github.com/ckeditor/ckeditor5/issues/16798. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/74f4571f186a2cbb30a8d3fcb62475c89f59c641)) +* Almost all dependencies of `ckeditor5-dev-*` packages have been bumped to their latest versions. ([commit](https://github.com/ckeditor/ckeditor5-dev/commit/2358a19113eb80f6204f39a1d0e0411810283ef2)) ### Released packages @@ -211,22 +146,24 @@ Check out the [Versioning policy](https://ckeditor.com/docs/ckeditor5/latest/fra
Released packages (summary) +Releases containing new features: + +* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 + Other releases: -* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-release-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-release-tools/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/jsdoc-plugins](https://www.npmjs.com/package/@ckeditor/jsdoc-plugins/v/42.0.0): v41.0.0 => v42.0.0 -* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/42.0.0): v41.0.0 => v42.0.0 +* [@ckeditor/ckeditor5-dev-build-tools](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-build-tools/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-bump-year](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-bump-year/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-ci](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-ci/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-dependency-checker](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-dependency-checker/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-docs](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-docs/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-stale-bot](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-stale-bot/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-tests](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-tests/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-transifex](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-transifex/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-translations](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-utils](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/ckeditor5-dev-web-crawler](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1 +* [@ckeditor/typedoc-plugins](https://www.npmjs.com/package/@ckeditor/typedoc-plugins/v/44.0.0-alpha.1): v44.0.0-alpha.0 => v44.0.0-alpha.1
--- diff --git a/README.md b/README.md index 1e44996f3..16dfab7aa 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,7 @@ This repository is a monorepo. It contains multiple npm packages. | [`@ckeditor/ckeditor5-dev-utils`](/packages/ckeditor5-dev-utils) | [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-dev-utils.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-utils) | | [`@ckeditor/ckeditor5-dev-translations`](/packages/ckeditor5-dev-translations) | [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-dev-translations.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-translations) | | [`@ckeditor/ckeditor5-dev-web-crawler`](/packages/ckeditor5-dev-web-crawler) | [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-dev-web-crawler.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-web-crawler) | -| [`@ckeditor/jsdoc-plugins`](/packages/jsdoc-plugins) | [![npm version](https://badge.fury.io/js/%40ckeditor%2Fjsdoc-plugins.svg)](https://www.npmjs.com/package/@ckeditor/jsdoc-plugins) | -| [`@ckeditor/typedoc-plugins`](/packages/typedoc-plugins) | [![npm version](https://badge.fury.io/js/%40ckeditor%2Ftypedoc-plugins.svg)](https://www.npmjs.com/package/@ckeditor/jsdoc-plugins) | +| [`@ckeditor/typedoc-plugins`](/packages/typedoc-plugins) | [![npm version](https://badge.fury.io/js/%40ckeditor%2Ftypedoc-plugins.svg)](https://www.npmjs.com/package/@ckeditor/typedoc-plugins) | ## Cloning diff --git a/package.json b/package.json index cabf2e784..81e798001 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ckeditor5-dev", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "private": true, "author": "CKSource (http://cksource.com/)", "license": "GPL-2.0-or-later", @@ -13,21 +13,21 @@ "engines": { "node": ">=18.0.0" }, + "type": "module", "devDependencies": { - "@ckeditor/ckeditor5-dev-ci": "^43.0.0", - "@ckeditor/ckeditor5-dev-release-tools": "^43.0.0", - "@ckeditor/ckeditor5-dev-bump-year": "^43.0.0", - "coveralls": "^3.1.1", - "eslint": "^7.0.0", - "eslint-config-ckeditor5": "^6.0.0", - "fs-extra": "^11.2.0", - "glob": "^10.2.5", + "@ckeditor/ckeditor5-dev-ci": "^44.0.0-alpha.5", + "@ckeditor/ckeditor5-dev-release-tools": "^44.0.0-alpha.5", + "@ckeditor/ckeditor5-dev-bump-year": "^44.0.0-alpha.5", + "eslint": "^8.21.0", + "eslint-config-ckeditor5": "^7.0.0", + "fs-extra": "^11.0.0", + "glob": "^10.0.0", "husky": "^8.0.2", - "lint-staged": "^10.2.4", - "listr2": "^6.5.0", + "js-yaml": "^4.1.0", + "lint-staged": "^15.0.0", + "listr2": "^8.0.0", "minimist": "^1.2.8", - "nyc": "^15.1.0", - "semver": "^7.5.3", + "semver": "^7.6.3", "upath": "^2.0.1" }, "scripts": { diff --git a/packages/ckeditor5-dev-build-tools/package.json b/packages/ckeditor5-dev-build-tools/package.json index 27e3cdf49..047f4b115 100644 --- a/packages/ckeditor5-dev-build-tools/package.json +++ b/packages/ckeditor5-dev-build-tools/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-build-tools", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "Rollup-based tools used to build CKEditor 5 packages.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -35,17 +35,17 @@ "@rollup/plugin-typescript": "^11.1.6", "@rollup/pluginutils": "^5.1.0", "@swc/core": "^1.4.8", - "chalk": "^5.3.0", - "cssnano": "^7.0.4", + "chalk": "^5.0.0", + "cssnano": "^7.0.0", "cssnano-preset-lite": "^4.0.1", "estree-walker": "^3.0.3", - "glob": "^10.3.10", + "glob": "^10.0.0", "lodash-es": "^4.17.21", "magic-string": "^0.30.6", "pofile": "^1.1.4", "postcss": "^8.0.0", - "postcss-mixins": "^9.0.4", - "postcss-nesting": "^12.0.2", + "postcss-mixins": "^11.0.0", + "postcss-nesting": "^13.0.0", "purgecss": "^6.0.0", "rollup": "^4.9.5", "rollup-plugin-styles": "^4.0.0", diff --git a/packages/ckeditor5-dev-build-tools/src/build.ts b/packages/ckeditor5-dev-build-tools/src/build.ts index 2814c84e0..e9303e406 100644 --- a/packages/ckeditor5-dev-build-tools/src/build.ts +++ b/packages/ckeditor5-dev-build-tools/src/build.ts @@ -104,6 +104,7 @@ async function generateUmdBuild( args: BuildOptions, bundle: RollupOutput ): Pro args.input = args.output; const { dir, name } = path.parse( args.output ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { plugins, ...config } = await getRollupConfig( args ); /** diff --git a/packages/ckeditor5-dev-build-tools/src/config.ts b/packages/ckeditor5-dev-build-tools/src/config.ts index 586d4a46e..cd20ccec8 100644 --- a/packages/ckeditor5-dev-build-tools/src/config.ts +++ b/packages/ckeditor5-dev-build-tools/src/config.ts @@ -152,7 +152,8 @@ export async function getRollupConfig( options: BuildOptions ) { plugins: [ postcssMixins, postcssNesting( { - noIsPseudoSelector: true + noIsPseudoSelector: true, + edition: '2021' } ) ], minimize: minify, diff --git a/packages/ckeditor5-dev-build-tools/src/plugins/translations.ts b/packages/ckeditor5-dev-build-tools/src/plugins/translations.ts index 1a53b59e5..5abb33aac 100644 --- a/packages/ckeditor5-dev-build-tools/src/plugins/translations.ts +++ b/packages/ckeditor5-dev-build-tools/src/plugins/translations.ts @@ -11,7 +11,7 @@ import PO from 'pofile'; import { groupBy, merge } from 'lodash-es'; import { glob } from 'glob'; import type { Plugin } from 'rollup'; -import { removeWhitespace } from '../utils'; +import { removeWhitespace } from '../utils.js'; const TYPINGS = removeWhitespace( ` import type { Translations } from '@ckeditor/ckeditor5-utils'; diff --git a/packages/ckeditor5-dev-build-tools/tests/_utils/utils.ts b/packages/ckeditor5-dev-build-tools/tests/_utils/utils.ts index 52873193b..75688e0a1 100644 --- a/packages/ckeditor5-dev-build-tools/tests/_utils/utils.ts +++ b/packages/ckeditor5-dev-build-tools/tests/_utils/utils.ts @@ -6,7 +6,7 @@ import { expect, vi } from 'vitest'; import swc from '@rollup/plugin-swc'; import type { RollupOutput, OutputChunk, OutputAsset, Plugin } from 'rollup'; -import * as utils from '../../src/utils'; +import * as utils from '../../src/utils.js'; /** * Helper function for validating Rollup asset. diff --git a/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts b/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts index 85f597361..d91a25f68 100644 --- a/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts +++ b/packages/ckeditor5-dev-build-tools/tests/build/build.test.ts @@ -64,7 +64,8 @@ async function mockCommercialDependencies() { () => ( { name: 'ckeditor5-premium-features', dependencies: { - '@ckeditor/ckeditor5-ai': '*' + '@ckeditor/ckeditor5-ai': '*', + 'ckeditor5-collaboration': '*' } } ) ); diff --git a/packages/ckeditor5-dev-bump-year/lib/bumpyear.js b/packages/ckeditor5-dev-bump-year/lib/bumpyear.js index f10dfe25f..b91a7cbd2 100644 --- a/packages/ckeditor5-dev-bump-year/lib/bumpyear.js +++ b/packages/ckeditor5-dev-bump-year/lib/bumpyear.js @@ -3,11 +3,9 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const chalk = require( 'chalk' ); -const fs = require( 'fs' ); -const { globSync } = require( 'glob' ); +import chalk from 'chalk'; +import fs from 'fs'; +import { globSync } from 'glob'; /** * Updates year in all licenses in the provided directory, based on provided glob patterns. @@ -18,13 +16,13 @@ const { globSync } = require( 'glob' ); * With: * Copyright (c) [initial year]-[current year] * - * @param {Object} params - * @param {String} params.cwd Current working directory from which all paths will be resolved. - * @param {Array} params.globPatterns An array of objects, where each object has string property `pattern`, + * @param {object} params + * @param {string} params.cwd Current working directory from which all paths will be resolved. + * @param {Array.} params.globPatterns An array of objects, where each object has string property `pattern`, * and optionally `options` property for this `glob` pattern. - * @param {String} [params.initialYear='2003'] Year from which the licenses should begin. + * @param {string} [params.initialYear='2003'] Year from which the licenses should begin. */ -module.exports = function bumpYear( params ) { +export default function bumpYear( params ) { if ( !params.initialYear ) { params.initialYear = '2003'; } @@ -92,13 +90,13 @@ module.exports = function bumpYear( params ) { console.log( file ); } } -}; +} /** * License headers are only required in JS and TS files. * - * @param {String} fileName - * @returns {Boolean} + * @param {string} fileName + * @returns {boolean} */ function isLicenseHeaderRequired( fileName ) { if ( fileName.endsWith( '.js' ) ) { diff --git a/packages/ckeditor5-dev-bump-year/lib/index.js b/packages/ckeditor5-dev-bump-year/lib/index.js index 4e20ce9e0..26fc3942f 100644 --- a/packages/ckeditor5-dev-bump-year/lib/index.js +++ b/packages/ckeditor5-dev-bump-year/lib/index.js @@ -3,10 +3,4 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const bumpYear = require( './bumpyear' ); - -module.exports = { - bumpYear -}; +export { default as bumpYear } from './bumpyear.js'; diff --git a/packages/ckeditor5-dev-bump-year/package.json b/packages/ckeditor5-dev-bump-year/package.json index 0784b1a2e..51e419bab 100644 --- a/packages/ckeditor5-dev-bump-year/package.json +++ b/packages/ckeditor5-dev-bump-year/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-bump-year", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "Used to bump year in the licence text specified at the top of the file.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -17,11 +17,12 @@ "npm": ">=5.7.1" }, "main": "lib/index.js", + "type": "module", "files": [ "lib" ], "dependencies": { - "chalk": "^4.1.0", - "glob": "^10.2.5" + "chalk": "^5.0.0", + "glob": "^10.0.0" } } diff --git a/packages/ckeditor5-dev-ci/bin/circle-disable-auto-cancel-builds.js b/packages/ckeditor5-dev-ci/bin/circle-disable-auto-cancel-builds.js index e27c08ff4..7e778b867 100755 --- a/packages/ckeditor5-dev-ci/bin/circle-disable-auto-cancel-builds.js +++ b/packages/ckeditor5-dev-ci/bin/circle-disable-auto-cancel-builds.js @@ -5,9 +5,7 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const circleUpdateAutoCancelBuilds = require( '../lib/circle-update-auto-cancel-builds' ); +import circleUpdateAutoCancelBuilds from '../lib/circle-update-auto-cancel-builds.js'; /** * This script updates CircleCI settings to disable the "Auto-cancel redundant workflows" option. diff --git a/packages/ckeditor5-dev-ci/bin/circle-enable-auto-cancel-builds.js b/packages/ckeditor5-dev-ci/bin/circle-enable-auto-cancel-builds.js index c5c1b9d06..15ae8cf69 100755 --- a/packages/ckeditor5-dev-ci/bin/circle-enable-auto-cancel-builds.js +++ b/packages/ckeditor5-dev-ci/bin/circle-enable-auto-cancel-builds.js @@ -5,9 +5,7 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const circleUpdateAutoCancelBuilds = require( '../lib/circle-update-auto-cancel-builds' ); +import circleUpdateAutoCancelBuilds from '../lib/circle-update-auto-cancel-builds.js'; /** * This script updates CircleCI settings to enable the "Auto-cancel redundant workflows" option. diff --git a/packages/ckeditor5-dev-ci/bin/circle-workflow-notifier.js b/packages/ckeditor5-dev-ci/bin/circle-workflow-notifier.js index 1b80a27d1..3d6382c6b 100755 --- a/packages/ckeditor5-dev-ci/bin/circle-workflow-notifier.js +++ b/packages/ckeditor5-dev-ci/bin/circle-workflow-notifier.js @@ -7,12 +7,9 @@ /* eslint-env node */ -'use strict'; - -const { execSync } = require( 'child_process' ); -const fetch = require( 'node-fetch' ); -const minimist = require( 'minimist' ); -const processJobStatuses = require( '../lib/process-job-statuses' ); +import { execSync } from 'child_process'; +import minimist from 'minimist'; +import processJobStatuses from '../lib/process-job-statuses.js'; // This script allows the creation of a new job within a workflow that will be executed // in the end, when all other jobs will be finished or errored. @@ -107,7 +104,12 @@ async function waitForOtherJobsAndSendNotification() { */ async function getOtherJobsData() { const url = `https://circleci.com/api/v2/workflow/${ CIRCLE_WORKFLOW_ID }/job`; - const options = { headers: { 'Circle-Token': CKE5_CIRCLE_TOKEN } }; + const options = { + method: 'GET', + headers: { + 'Circle-Token': CKE5_CIRCLE_TOKEN + } + }; const response = await fetch( url, options ); const data = await response.json(); @@ -116,10 +118,10 @@ async function getOtherJobsData() { } /** - * @param {Array.} args - * @returns {Object} result - * @returns {String} result.task - * @returns {Array} result.ignore + * @param {Array.} args + * @returns {object} result + * @returns {string} result.task + * @returns {Array.} result.ignore */ function parseArguments( args ) { const config = { diff --git a/packages/ckeditor5-dev-ci/bin/is-job-triggered-by-member.js b/packages/ckeditor5-dev-ci/bin/is-job-triggered-by-member.js index 42e9a6584..adc296d0a 100755 --- a/packages/ckeditor5-dev-ci/bin/is-job-triggered-by-member.js +++ b/packages/ckeditor5-dev-ci/bin/is-job-triggered-by-member.js @@ -5,9 +5,7 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const isJobTriggeredByMember = require( '../lib/is-job-triggered-by-member' ); +import isJobTriggeredByMember from '../lib/is-job-triggered-by-member.js'; /** * This script checks if a user that approved an approval job could do that. diff --git a/packages/ckeditor5-dev-ci/bin/notify-circle-status.js b/packages/ckeditor5-dev-ci/bin/notify-circle-status.js index 6ee788746..3aed9a6cb 100755 --- a/packages/ckeditor5-dev-ci/bin/notify-circle-status.js +++ b/packages/ckeditor5-dev-ci/bin/notify-circle-status.js @@ -7,9 +7,8 @@ /* eslint-env node */ -const fetch = require( 'node-fetch' ); -const slackNotify = require( 'slack-notify' ); -const formatMessage = require( '../lib/format-message' ); +import slackNotify from 'slack-notify'; +import formatMessage from '../lib/format-message.js'; // This script assumes that is being executed on Circle CI. // Step it is used on should have set value: `when: on_fail`, since it does not diff --git a/packages/ckeditor5-dev-ci/bin/notify-travis-status.js b/packages/ckeditor5-dev-ci/bin/notify-travis-status.js index eb1bd479f..93b6923f9 100755 --- a/packages/ckeditor5-dev-ci/bin/notify-travis-status.js +++ b/packages/ckeditor5-dev-ci/bin/notify-travis-status.js @@ -7,8 +7,8 @@ /* eslint-env node */ -const formatMessage = require( '../lib/format-message' ); -const slackNotify = require( 'slack-notify' ); +import formatMessage from '../lib/format-message.js'; +import slackNotify from 'slack-notify'; const ALLOWED_BRANCHES = [ 'stable', diff --git a/packages/ckeditor5-dev-ci/bin/trigger-circle-build.js b/packages/ckeditor5-dev-ci/bin/trigger-circle-build.js index eb0b43d3c..17fd1da10 100755 --- a/packages/ckeditor5-dev-ci/bin/trigger-circle-build.js +++ b/packages/ckeditor5-dev-ci/bin/trigger-circle-build.js @@ -5,9 +5,7 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const triggerCircleBuild = require( '../lib/trigger-circle-build' ); +import triggerCircleBuild from '../lib/trigger-circle-build.js'; /** * This script triggers a new CircleCI build. diff --git a/packages/ckeditor5-dev-ci/lib/circle-update-auto-cancel-builds.js b/packages/ckeditor5-dev-ci/lib/circle-update-auto-cancel-builds.js index 46da393d5..351d115b7 100644 --- a/packages/ckeditor5-dev-ci/lib/circle-update-auto-cancel-builds.js +++ b/packages/ckeditor5-dev-ci/lib/circle-update-auto-cancel-builds.js @@ -3,19 +3,15 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fetch = require( 'node-fetch' ); - /** * @param options - * @param {String} options.circleToken - * @param {String} options.githubOrganization - * @param {String} options.githubRepository - * @param {Boolean} options.newValue - * @return {Promise.} + * @param {string} options.circleToken + * @param {string} options.githubOrganization + * @param {string} options.githubRepository + * @param {boolean} options.newValue + * @returns {Promise.} */ -module.exports = async function circleUpdateAutoCancelBuilds( options ) { +export default async function circleUpdateAutoCancelBuilds( options ) { const { circleToken, githubOrganization, @@ -24,7 +20,7 @@ module.exports = async function circleUpdateAutoCancelBuilds( options ) { } = options; const circleRequestOptions = { - method: 'patch', + method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Circle-Token': circleToken @@ -40,4 +36,4 @@ module.exports = async function circleUpdateAutoCancelBuilds( options ) { return fetch( settingsUpdateUrl, circleRequestOptions ) .then( r => r.json() ); -}; +} diff --git a/packages/ckeditor5-dev-ci/lib/data/index.js b/packages/ckeditor5-dev-ci/lib/data/index.js new file mode 100644 index 000000000..f3dd882ac --- /dev/null +++ b/packages/ckeditor5-dev-ci/lib/data/index.js @@ -0,0 +1,10 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { default as _members } from './members.json' with { type: 'json' }; +import { default as _bots } from './bots.json' with { type: 'json' }; + +export const members = _members; +export const bots = _bots; diff --git a/packages/ckeditor5-dev-ci/lib/format-message.js b/packages/ckeditor5-dev-ci/lib/format-message.js index 35b900348..1d69bc66e 100644 --- a/packages/ckeditor5-dev-ci/lib/format-message.js +++ b/packages/ckeditor5-dev-ci/lib/format-message.js @@ -5,31 +5,27 @@ /* eslint-env node */ -'use strict'; - -const fetch = require( 'node-fetch' ); -const bots = require( './data/bots.json' ); -const members = require( './data/members.json' ); +import { bots, members } from './data/index.js'; const REPOSITORY_REGEXP = /github\.com\/([^/]+)\/([^/]+)/; /** - * @param {Object} options - * @param {String} options.slackMessageUsername - * @param {String} options.iconUrl - * @param {String} options.repositoryOwner - * @param {String} options.repositoryName - * @param {String} options.branch - * @param {String} options.buildTitle - * @param {String} options.buildUrl - * @param {String} options.buildId - * @param {String} options.githubToken - * @param {String} options.triggeringCommitUrl - * @param {Number} options.startTime - * @param {Number} options.endTime - * @param {Boolean} options.shouldHideAuthor + * @param {object} options + * @param {string} options.slackMessageUsername + * @param {string} options.iconUrl + * @param {string} options.repositoryOwner + * @param {string} options.repositoryName + * @param {string} options.branch + * @param {string} options.buildTitle + * @param {string} options.buildUrl + * @param {string} options.buildId + * @param {string} options.githubToken + * @param {string} options.triggeringCommitUrl + * @param {number} options.startTime + * @param {number} options.endTime + * @param {boolean} options.shouldHideAuthor */ -module.exports = async function formatMessage( options ) { +export default async function formatMessage( options ) { const commitDetails = await getCommitDetails( options.triggeringCommitUrl, options.githubToken ); const repoUrl = `https://github.com/${ options.repositoryOwner }/${ options.repositoryName }`; @@ -63,16 +59,16 @@ module.exports = async function formatMessage( options ) { } ] } ] }; -}; +} /** * Returns the additional message that will be added to the notifier post. * - * @param {Object} options - * @param {Boolean} options.shouldHideAuthor - * @param {String|null} options.githubAccount - * @param {String} options.commitAuthor - * @returns {String} + * @param {object} options + * @param {boolean} options.shouldHideAuthor + * @param {string|null} options.githubAccount + * @param {string} options.commitAuthor + * @returns {string} */ function getNotifierMessage( options ) { if ( options.shouldHideAuthor ) { @@ -99,8 +95,8 @@ function getNotifierMessage( options ) { } /** - * @param {String|null} githubAccount - * @returns {String|null} + * @param {string|null} githubAccount + * @returns {string|null} */ function findSlackAccount( githubAccount ) { if ( !githubAccount ) { @@ -120,9 +116,9 @@ function findSlackAccount( githubAccount ) { * Returns string representing amount of time passed between two timestamps. * Timestamps should be in seconds instead of milliseconds. * - * @param {Number} startTime - * @param {Number} endTime - * @returns {String} + * @param {number} startTime + * @param {number} endTime + * @returns {string} */ function getExecutionTime( startTime, endTime ) { if ( !startTime || !endTime ) { @@ -159,8 +155,8 @@ function getExecutionTime( startTime, endTime ) { /** * Replaces `#Id` and `Repo/Owner#Id` with URls to Github Issues. * - * @param {String} commitMessage - * @param {String} triggeringCommitUrl + * @param {string} commitMessage + * @param {string} triggeringCommitUrl * @returns {string} */ function getFormattedMessage( commitMessage, triggeringCommitUrl ) { @@ -182,9 +178,9 @@ function getFormattedMessage( commitMessage, triggeringCommitUrl ) { /** * Returns a promise that resolves the commit details (author and message) based on the specified GitHub URL. * - * @param {String} triggeringCommitUrl The URL to the commit on GitHub. - * @param {String} githubToken Github token used for authorization a request, - * @returns {Promise.} + * @param {string} triggeringCommitUrl The URL to the commit on GitHub. + * @param {string} githubToken Github token used for authorization a request, + * @returns {Promise.} */ function getCommitDetails( triggeringCommitUrl, githubToken ) { const apiGithubUrlCommit = getGithubApiUrl( triggeringCommitUrl ); @@ -209,8 +205,8 @@ function getCommitDetails( triggeringCommitUrl, githubToken ) { /** * Returns a URL to GitHub API which returns details of the commit that caused the CI to fail its job. * - * @param {String} triggeringCommitUrl The URL to the commit on GitHub. - * @returns {String} + * @param {string} triggeringCommitUrl The URL to the commit on GitHub. + * @returns {string} */ function getGithubApiUrl( triggeringCommitUrl ) { return triggeringCommitUrl.replace( 'github.com/', 'api.github.com/repos/' ).replace( '/commit/', '/commits/' ); diff --git a/packages/ckeditor5-dev-ci/lib/index.js b/packages/ckeditor5-dev-ci/lib/index.js index 6b714f736..a31027300 100644 --- a/packages/ckeditor5-dev-ci/lib/index.js +++ b/packages/ckeditor5-dev-ci/lib/index.js @@ -3,9 +3,5 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -module.exports = { - getJobApprover: require( './utils/get-job-approver' ), - members: require( './data/members.json' ) -}; +export { default as getJobApprover } from './utils/get-job-approver.js'; +export { members } from './data/index.js'; diff --git a/packages/ckeditor5-dev-ci/lib/is-job-triggered-by-member.js b/packages/ckeditor5-dev-ci/lib/is-job-triggered-by-member.js index 185218ccc..2c9ead7d4 100644 --- a/packages/ckeditor5-dev-ci/lib/is-job-triggered-by-member.js +++ b/packages/ckeditor5-dev-ci/lib/is-job-triggered-by-member.js @@ -3,22 +3,20 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { Octokit } = require( '@octokit/rest' ); -const getJobApprover = require( './utils/get-job-approver' ); +import { Octokit } from '@octokit/rest'; +import getJobApprover from './utils/get-job-approver.js'; /** * @param options - * @param {String} options.circleToken - * @param {String} options.circleWorkflowId - * @param {String} options.circleApprovalJobName - * @param {String} options.githubOrganization - * @param {String} options.githubTeamSlug - * @param {String} options.githubToken - * @return {Promise.} + * @param {string} options.circleToken + * @param {string} options.circleWorkflowId + * @param {string} options.circleApprovalJobName + * @param {string} options.githubOrganization + * @param {string} options.githubTeamSlug + * @param {string} options.githubToken + * @returns {Promise.} */ -module.exports = async function isJobTriggeredByMember( options ) { +export default async function isJobTriggeredByMember( options ) { const { circleToken, circleWorkflowId, @@ -41,4 +39,4 @@ module.exports = async function isJobTriggeredByMember( options ) { return data .map( ( { login } ) => login ) .includes( login ); -}; +} diff --git a/packages/ckeditor5-dev-ci/lib/process-job-statuses.js b/packages/ckeditor5-dev-ci/lib/process-job-statuses.js index 6d8b69055..4444d6959 100644 --- a/packages/ckeditor5-dev-ci/lib/process-job-statuses.js +++ b/packages/ckeditor5-dev-ci/lib/process-job-statuses.js @@ -3,8 +3,6 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /** * The function aims to determine a proper build status for children's jobs based on their parent's statuses. * @@ -19,7 +17,7 @@ * @param {Array.} jobs * @returns {Array.} */ -module.exports = function processJobStatuses( jobs ) { +export default function processJobStatuses( jobs ) { // To avoid modifying the original object, let's clone. const jobsClone = clone( jobs ); @@ -55,11 +53,11 @@ module.exports = function processJobStatuses( jobs ) { } return jobsClone; -}; +} /** * @param {WorkflowJob} job - * @returns {Boolean} + * @returns {boolean} */ function isJobFailed( job ) { if ( job.status === 'failed' ) { @@ -83,11 +81,11 @@ function clone( obj ) { } /** - * @typedef {Object} WorkflowJob + * @typedef {object} WorkflowJob * - * @property {String} id + * @property {string} id * * @property {'blocked'|'running'|'failed'|'failed_parent'|'success'} status * - * @property {Array.} dependencies + * @property {Array.} dependencies */ diff --git a/packages/ckeditor5-dev-ci/lib/trigger-circle-build.js b/packages/ckeditor5-dev-ci/lib/trigger-circle-build.js index 4a7504578..a30ba7137 100644 --- a/packages/ckeditor5-dev-ci/lib/trigger-circle-build.js +++ b/packages/ckeditor5-dev-ci/lib/trigger-circle-build.js @@ -5,19 +5,17 @@ /* eslint-env node */ -const fetch = require( 'node-fetch' ); - /** * @param options - * @param {String} options.circleToken - * @param {String} options.commit - * @param {String} options.branch - * @param {String} options.repositorySlug A repository slug (org/name) where a new build will be started. - * @param {String|null} [options.releaseBranch=null] Define a branch that leads the release process. - * @param {String|null} [options.triggerRepositorySlug=null] A repository slug (org/name) that triggers a new build. - * @return {Promise} + * @param {string} options.circleToken + * @param {string} options.commit + * @param {string} options.branch + * @param {string} options.repositorySlug A repository slug (org/name) where a new build will be started. + * @param {string|null} [options.releaseBranch=null] Define a branch that leads the release process. + * @param {string|null} [options.triggerRepositorySlug=null] A repository slug (org/name) that triggers a new build. + * @returns {Promise} */ -module.exports = async function triggerCircleBuild( options ) { +export default async function triggerCircleBuild( options ) { const { circleToken, commit, @@ -42,7 +40,7 @@ module.exports = async function triggerCircleBuild( options ) { } const requestOptions = { - method: 'post', + method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -62,4 +60,4 @@ module.exports = async function triggerCircleBuild( options ) { throw new Error( `CI trigger failed: "${ response.message }".` ); } } ); -}; +} diff --git a/packages/ckeditor5-dev-ci/lib/utils/get-job-approver.js b/packages/ckeditor5-dev-ci/lib/utils/get-job-approver.js index 37998992d..9eb2b4175 100644 --- a/packages/ckeditor5-dev-ci/lib/utils/get-job-approver.js +++ b/packages/ckeditor5-dev-ci/lib/utils/get-job-approver.js @@ -3,21 +3,17 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fetch = require( 'node-fetch' ); - /** * Returns a promise that resolves to GitHub name of a developer who approved the `jobName` job. * - * @param {String} circleCiToken - * @param {String} workflowId - * @param {String} jobName - * @returns {Promise.} + * @param {string} circleCiToken + * @param {string} workflowId + * @param {string} jobName + * @returns {Promise.} */ -module.exports = async function getJobApprover( circleCiToken, workflowId, jobName ) { +export default async function getJobApprover( circleCiToken, workflowId, jobName ) { const circleRequestOptions = { - method: 'get', + method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -35,4 +31,4 @@ module.exports = async function getJobApprover( circleCiToken, workflowId, jobNa const { login } = await fetch( userDetailsUrl, circleRequestOptions ).then( r => r.json() ); return login; -}; +} diff --git a/packages/ckeditor5-dev-ci/package.json b/packages/ckeditor5-dev-ci/package.json index fe43879d0..44154192c 100644 --- a/packages/ckeditor5-dev-ci/package.json +++ b/packages/ckeditor5-dev-ci/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-ci", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "Utils used on various Continuous Integration services.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -16,6 +16,7 @@ "node": ">=18.0.0", "npm": ">=5.7.1" }, + "type": "module", "main": "lib/index.js", "files": [ "bin", @@ -33,20 +34,15 @@ "ckeditor5-dev-ci-circle-enable-auto-cancel-builds": "bin/circle-enable-auto-cancel-builds.js" }, "dependencies": { - "@octokit/rest": "^19.0.0", + "@octokit/rest": "^21.0.0", "minimist": "^1.2.8", - "node-fetch": "^2.6.7", "slack-notify": "^2.0.6" }, "devDependencies": { - "chai": "^4.2.0", - "mocha": "^7.1.2", - "proxyquire": "^2.1.3", - "mockery": "^2.1.0", - "sinon": "^9.2.4" + "vitest": "^2.0.5" }, "scripts": { - "test": "mocha './tests/**/*.js' --timeout 10000", - "coverage": "nyc --reporter=lcov --reporter=text-summary yarn run test" + "test": "vitest run --config vitest.config.js", + "coverage": "vitest run --config vitest.config.js --coverage" } } diff --git a/packages/ckeditor5-dev-ci/tests/circle-update-auto-cancel-builds.js b/packages/ckeditor5-dev-ci/tests/circle-update-auto-cancel-builds.js index 66c0ee3db..05d988ca0 100644 --- a/packages/ckeditor5-dev-ci/tests/circle-update-auto-cancel-builds.js +++ b/packages/ckeditor5-dev-ci/tests/circle-update-auto-cancel-builds.js @@ -3,41 +3,17 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +import { describe, expect, it, vi } from 'vitest'; +import circleUpdateAutoCancelBuilds from '../lib/circle-update-auto-cancel-builds.js'; describe( 'lib/circleUpdateAutoCancelBuilds', () => { - let stubs, circleUpdateAutoCancelBuilds; - - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - fetch: sinon.stub() - }; - - mockery.registerMock( 'node-fetch', stubs.fetch ); - - circleUpdateAutoCancelBuilds = require( '../lib/circle-update-auto-cancel-builds' ); - } ); - - afterEach( () => { - mockery.disable(); - } ); - it( 'should send a request to CircleCI to update the redundant workflows option', async () => { - const response = {}; + const response = { foo: 'bar' }; - stubs.fetch.resolves( { - json: () => Promise.resolve( response ) - } ); + const fetchMock = vi.fn(); + vi.stubGlobal( 'fetch', fetchMock ); + + fetchMock.mockResolvedValue( { json: () => Promise.resolve( response ) } ); const results = await circleUpdateAutoCancelBuilds( { circleToken: 'circle-token', @@ -46,19 +22,23 @@ describe( 'lib/circleUpdateAutoCancelBuilds', () => { newValue: true } ); - expect( stubs.fetch.callCount ).to.equal( 1 ); expect( results ).to.deep.equal( response ); - const [ url, options ] = stubs.fetch.firstCall.args; - - expect( url ).to.equal( 'https://circleci.com/api/v2/project/github/ckeditor/ckeditor5-foo/settings' ); - expect( options ).to.have.property( 'method', 'patch' ); - expect( options ).to.have.property( 'headers' ); - expect( options.headers ).to.have.property( 'Circle-Token', 'circle-token' ); - expect( options ).to.have.property( 'body' ); - - const body = JSON.parse( options.body ); - expect( body ).to.have.property( 'advanced' ); - expect( body.advanced ).to.have.property( 'autocancel_builds', true ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledWith( + 'https://circleci.com/api/v2/project/github/ckeditor/ckeditor5-foo/settings', + { + method: 'PATCH', + headers: { + 'Circle-Token': 'circle-token', + 'Content-Type': 'application/json' + }, + body: JSON.stringify( { + advanced: { + 'autocancel_builds': true + } + } ) + } + ); } ); } ); diff --git a/packages/ckeditor5-dev-ci/tests/data/members.js b/packages/ckeditor5-dev-ci/tests/data/members.js index 8dfa6ecab..250eba439 100644 --- a/packages/ckeditor5-dev-ci/tests/data/members.js +++ b/packages/ckeditor5-dev-ci/tests/data/members.js @@ -3,15 +3,11 @@ * For licensing, see LICENSE.md. */ -/* eslint-env node */ - -'use strict'; - -const members = require( '../../lib/data/members.json' ); -const expect = require( 'chai' ).expect; +import { describe, expect, it } from 'vitest'; +import { members } from '../../lib/data/index.js'; describe( 'lib/data/members', () => { - it( 'should be a function', () => { + it( 'should be an object', () => { expect( members ).to.be.a( 'object' ); } ); diff --git a/packages/ckeditor5-dev-ci/tests/format-message.js b/packages/ckeditor5-dev-ci/tests/format-message.js index c826497a1..3e6ae167d 100644 --- a/packages/ckeditor5-dev-ci/tests/format-message.js +++ b/packages/ckeditor5-dev-ci/tests/format-message.js @@ -5,35 +5,36 @@ /* eslint-env node */ -'use strict'; - -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +import formatMessage from '../lib/format-message.js'; + +vi.mock( '../lib/data', () => { + return { + members: { + ExampleNick: 'slackId' + }, + bots: [ + 'CKCSBot' + ] + }; +} ); describe( 'lib/format-message', () => { - let formatMessage, stubs; - - beforeEach( () => { - stubs = { - nodeFetch: sinon.stub() - }; - - formatMessage = proxyquire( '../lib/format-message', { - 'node-fetch': stubs.nodeFetch, - './data/members.json': { - ExampleNick: 'slackId' - } + describe( 'formatMessage()', () => { + let fetchMock; + + beforeEach( () => { + fetchMock = vi.fn(); + vi.stubGlobal( 'fetch', fetchMock ); } ); - } ); - describe( 'formatMessage()', () => { it( 'should be a function', () => { - expect( formatMessage ).to.be.a( 'function' ); + expect( formatMessage ).toBeInstanceOf( Function ); } ); it( 'should display a message for bot if a login is included in the "bots" array', async () => { - stubs.nodeFetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValueOnce( { json() { return Promise.resolve( { author: { @@ -72,7 +73,7 @@ describe( 'lib/format-message', () => { } ); it( 'should display a message for bot if a login is unavailable but author name is included in the "bots" array', async () => { - stubs.nodeFetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValueOnce( { json() { return Promise.resolve( { author: null, @@ -109,7 +110,7 @@ describe( 'lib/format-message', () => { } ); it( 'should mention the channel if a login is unavailable and author name is not included in the "bots" array', async () => { - stubs.nodeFetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValueOnce( { json() { return Promise.resolve( { author: null, @@ -146,7 +147,7 @@ describe( 'lib/format-message', () => { } ); it( 'should find a Slack account based on a GitHub account case-insensitive', async () => { - stubs.nodeFetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValueOnce( { json() { return Promise.resolve( { author: { @@ -181,7 +182,7 @@ describe( 'lib/format-message', () => { expect( message ).to.be.an( 'object' ); expect( message ).to.have.property( 'text' ); - expect( message.text ).to.equal( '<@slackId>, could you take a look?' ); + expect( message.text ).toEqual( '<@slackId>, could you take a look?' ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-ci/tests/is-job-triggered-by-member.js b/packages/ckeditor5-dev-ci/tests/is-job-triggered-by-member.js index b215d1b02..c5b18e61e 100644 --- a/packages/ckeditor5-dev-ci/tests/is-job-triggered-by-member.js +++ b/packages/ckeditor5-dev-ci/tests/is-job-triggered-by-member.js @@ -3,47 +3,38 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); -const proxyquire = require( 'proxyquire' ); - -describe( 'lib/isJobTriggeredByMember', () => { - let stubs, isJobTriggeredByMember; - - beforeEach( () => { - stubs = { - fetch: sinon.stub( global, 'fetch' ), - getJobApprover: sinon.stub(), - octokitRestInstance: { - request: sinon.stub() - }, - octokitRest: sinon.stub().callsFake( () => stubs.octokitRestInstance ) - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); +import { describe, expect, it, vi } from 'vitest'; +import isJobTriggeredByMember from '../lib/is-job-triggered-by-member.js'; +import getJobApprover from '../lib/utils/get-job-approver.js'; + +const { + octokitRequestMock, + octokitConstructorSpy +} = vi.hoisted( () => { + return { + octokitRequestMock: vi.fn(), + octokitConstructorSpy: vi.fn() + }; +} ); - mockery.registerMock( '@octokit/rest', { Octokit: stubs.octokitRest } ); +vi.mock( '../lib/utils/get-job-approver' ); - isJobTriggeredByMember = proxyquire( '../lib/is-job-triggered-by-member', { - './utils/get-job-approver': stubs.getJobApprover - } ); - } ); +vi.mock( '@octokit/rest', () => { + return { + Octokit: class { + constructor( ...args ) { + octokitConstructorSpy( ...args ); - afterEach( () => { - mockery.disable(); - sinon.restore(); - } ); + this.request = octokitRequestMock; + } + } + }; +} ); +describe( 'lib/isJobTriggeredByMember', () => { it( 'should pass given parameters to services', async () => { - stubs.getJobApprover.resolves( 'foo' ); - stubs.octokitRestInstance.request.resolves( { data: [] } ); + vi.mocked( getJobApprover ).mockResolvedValue( 'foo' ); + vi.mocked( octokitRequestMock ).mockResolvedValue( { data: [] } ); await isJobTriggeredByMember( { circleToken: 'circle-token', @@ -54,38 +45,40 @@ describe( 'lib/isJobTriggeredByMember', () => { githubToken: 'github-token' } ); - expect( stubs.octokitRest.callCount ).to.equal( 1 ); - expect( stubs.octokitRest.firstCall.firstArg ).to.have.property( 'auth', 'github-token' ); + expect( vi.mocked( octokitConstructorSpy ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( octokitConstructorSpy ) ).toHaveBeenCalledWith( { + 'auth': 'github-token' + } ); - expect( stubs.getJobApprover.callCount ).to.equal( 1 ); - expect( stubs.getJobApprover.firstCall.args ).to.deep.equal( [ + expect( vi.mocked( getJobApprover ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( getJobApprover ) ).toHaveBeenCalledWith( 'circle-token', 'abc-123-abc-456', 'approval-job' - ] ); - - expect( stubs.octokitRestInstance.request.callCount ).to.equal( 1 ); - - const [ url, data ] = stubs.octokitRestInstance.request.firstCall.args; - - expect( url ).to.equal( 'GET /orgs/{org}/teams/{team_slug}/members' ); - expect( data ).to.have.property( 'org', 'ckeditor' ); - expect( data ).to.have.property( 'team_slug', 'team-slug' ); - expect( data ).to.have.property( 'headers' ); - expect( data.headers ).to.have.property( 'X-GitHub-Api-Version', '2022-11-28' ); + ); + + expect( vi.mocked( octokitRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( octokitRequestMock ) ).toHaveBeenCalledWith( + 'GET /orgs/{org}/teams/{team_slug}/members', + { + 'org': 'ckeditor', + 'team_slug': 'team-slug', + 'headers': { + 'X-GitHub-Api-Version': '2022-11-28' + } + } + ); } ); it( 'should resolves true when a team member is allowed to trigger the given job', async () => { // Who triggered. - stubs.getJobApprover.resolves( 'foo' ); + vi.mocked( getJobApprover ).mockResolvedValue( 'foo' ); // Who is allowed to trigger. - stubs.octokitRestInstance.request.resolves( { - data: [ - { login: 'foo' }, - { login: 'bar' } - ] - } ); + vi.mocked( octokitRequestMock ).mockResolvedValue( { data: [ + { login: 'foo' }, + { login: 'bar' } + ] } ); const result = await isJobTriggeredByMember( { circleToken: 'circle-token', @@ -96,19 +89,17 @@ describe( 'lib/isJobTriggeredByMember', () => { githubToken: 'github-token' } ); - expect( result ).to.equal( true ); + expect( result ).toEqual( true ); } ); it( 'should resolves false when a team member is not allowed to trigger the given job', async () => { // Who triggered. - stubs.getJobApprover.resolves( 'foo' ); + vi.mocked( getJobApprover ).mockResolvedValue( 'foo' ); // Who is allowed to trigger. - stubs.octokitRestInstance.request.resolves( { - data: [ - { login: 'bar' } - ] - } ); + vi.mocked( octokitRequestMock ).mockResolvedValue( { data: [ + { login: 'bar' } + ] } ); const result = await isJobTriggeredByMember( { circleToken: 'circle-token', @@ -119,6 +110,6 @@ describe( 'lib/isJobTriggeredByMember', () => { githubToken: 'github-token' } ); - expect( result ).to.equal( false ); + expect( result ).toEqual( false ); } ); } ); diff --git a/packages/ckeditor5-dev-ci/tests/process-job-statuses.js b/packages/ckeditor5-dev-ci/tests/process-job-statuses.js index 12bc51027..0c0ade4a0 100644 --- a/packages/ckeditor5-dev-ci/tests/process-job-statuses.js +++ b/packages/ckeditor5-dev-ci/tests/process-job-statuses.js @@ -5,17 +5,10 @@ /* eslint-env node */ -'use strict'; - -const expect = require( 'chai' ).expect; +import { describe, expect, it } from 'vitest'; +import processJobStatuses from '../lib/process-job-statuses.js'; describe( 'lib/process-job-statuses', () => { - let processJobStatuses; - - beforeEach( () => { - processJobStatuses = require( '../lib/process-job-statuses' ); - } ); - describe( 'processJobStatuses()', () => { it( 'should be a function', () => { expect( processJobStatuses ).to.be.a( 'function' ); @@ -38,7 +31,7 @@ describe( 'lib/process-job-statuses', () => { dependencies: [] } ]; - expect( processJobStatuses( jobs ) ).to.deep.equal( expectedOutput ); + expect( processJobStatuses( jobs ) ).toEqual( expectedOutput ); } ); // Workflow: @@ -69,7 +62,7 @@ describe( 'lib/process-job-statuses', () => { dependencies: [ 'id1' ] } ]; - expect( processJobStatuses( jobs ) ).to.deep.equal( expectedOutput ); + expect( processJobStatuses( jobs ) ).toEqual( expectedOutput ); } ); // Workflow: @@ -97,7 +90,7 @@ describe( 'lib/process-job-statuses', () => { dependencies: [ 'id1' ] } ]; - expect( processJobStatuses( jobs ) ).to.deep.equal( expectedOutput ); + expect( processJobStatuses( jobs ) ).toEqual( expectedOutput ); } ); // Workflow: @@ -125,7 +118,7 @@ describe( 'lib/process-job-statuses', () => { dependencies: [ 'id1' ] } ]; - expect( processJobStatuses( jobs ) ).to.deep.equal( expectedOutput ); + expect( processJobStatuses( jobs ) ).toEqual( expectedOutput ); } ); // Workflow: @@ -169,7 +162,7 @@ describe( 'lib/process-job-statuses', () => { dependencies: [ 'id3' ] } ]; - expect( processJobStatuses( jobs ) ).to.deep.equal( expectedOutput ); + expect( processJobStatuses( jobs ) ).toEqual( expectedOutput ); } ); // Workflow: @@ -223,7 +216,7 @@ describe( 'lib/process-job-statuses', () => { dependencies: [ 'id_2', 'id_3' ] } ]; - expect( processJobStatuses( jobs ) ).to.deep.equal( expectedOutput ); + expect( processJobStatuses( jobs ) ).toEqual( expectedOutput ); } ); // Workflow: @@ -277,7 +270,7 @@ describe( 'lib/process-job-statuses', () => { dependencies: [ 'id_2', 'id_3' ] } ]; - expect( processJobStatuses( jobs ) ).to.deep.equal( expectedOutput ); + expect( processJobStatuses( jobs ) ).toEqual( expectedOutput ); } ); // Workflow: @@ -351,7 +344,7 @@ describe( 'lib/process-job-statuses', () => { dependencies: [ 'id_2', 'id_3', 'id_4', 'id_5' ] } ]; - expect( processJobStatuses( jobs ) ).to.deep.equal( expectedOutput ); + expect( processJobStatuses( jobs ) ).toEqual( expectedOutput ); } ); // Workflow: @@ -425,7 +418,7 @@ describe( 'lib/process-job-statuses', () => { dependencies: [ 'id_2', 'id_3', 'id_4', 'id_5' ] } ]; - expect( processJobStatuses( jobs ) ).to.deep.equal( expectedOutput ); + expect( processJobStatuses( jobs ) ).toEqual( expectedOutput ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-ci/tests/trigger-circle-build.js b/packages/ckeditor5-dev-ci/tests/trigger-circle-build.js index 3afb4dd42..4f973c5ab 100644 --- a/packages/ckeditor5-dev-ci/tests/trigger-circle-build.js +++ b/packages/ckeditor5-dev-ci/tests/trigger-circle-build.js @@ -3,37 +3,19 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import triggerCircleBuild from '../lib/trigger-circle-build.js'; describe( 'lib/triggerCircleBuild', () => { - let stubs, triggerCircleBuild; + let fetchMock; beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - fetch: sinon.stub() - }; - - mockery.registerMock( 'node-fetch', stubs.fetch ); - - triggerCircleBuild = require( '../lib/trigger-circle-build' ); - } ); - - afterEach( () => { - mockery.disable(); + fetchMock = vi.fn(); + vi.stubGlobal( 'fetch', fetchMock ); } ); it( 'should send a POST request to the CircleCI service', async () => { - stubs.fetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValue( { json: () => Promise.resolve( { error_message: null } ) @@ -46,24 +28,28 @@ describe( 'lib/triggerCircleBuild', () => { repositorySlug: 'ckeditor/ckeditor5-dev' } ); - expect( stubs.fetch.callCount ).to.equal( 1 ); - - const [ url, options ] = stubs.fetch.firstCall.args; - - expect( url ).to.equal( 'https://circleci.com/api/v2/project/github/ckeditor/ckeditor5-dev/pipeline' ); - expect( options ).to.have.property( 'method', 'post' ); - expect( options ).to.have.property( 'headers' ); - expect( options.headers ).to.have.property( 'Circle-Token', 'circle-token' ); - expect( options ).to.have.property( 'body' ); - - const body = JSON.parse( options.body ); - expect( body ).to.have.property( 'branch', 'master' ); - expect( body ).to.have.property( 'parameters' ); - expect( body.parameters ).to.have.property( 'triggerCommitHash', 'abcd1234' ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledWith( + 'https://circleci.com/api/v2/project/github/ckeditor/ckeditor5-dev/pipeline', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Circle-Token': 'circle-token', + 'Content-Type': 'application/json' + }, + body: JSON.stringify( { + branch: 'master', + parameters: { + triggerCommitHash: 'abcd1234' + } + } ) + } + ); } ); it( 'should include the "isRelease=true" parameter when passing the `releaseBranch` option (the same release branch)', async () => { - stubs.fetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValue( { json: () => Promise.resolve( { error_message: null } ) @@ -77,19 +63,29 @@ describe( 'lib/triggerCircleBuild', () => { releaseBranch: 'master' } ); - expect( stubs.fetch.callCount ).to.equal( 1 ); - - const [ , options ] = stubs.fetch.firstCall.args; - - expect( options ).to.have.property( 'body' ); - - const body = JSON.parse( options.body ); - expect( body ).to.have.property( 'parameters' ); - expect( body.parameters ).to.have.property( 'isRelease', true ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledWith( + 'https://circleci.com/api/v2/project/github/ckeditor/ckeditor5-dev/pipeline', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Circle-Token': 'circle-token', + 'Content-Type': 'application/json' + }, + body: JSON.stringify( { + branch: 'master', + parameters: { + triggerCommitHash: 'abcd1234', + isRelease: true + } + } ) + } + ); } ); it( 'should include the "isRelease=false" parameter when passing the `releaseBranch` option', async () => { - stubs.fetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValue( { json: () => Promise.resolve( { error_message: null } ) @@ -103,19 +99,29 @@ describe( 'lib/triggerCircleBuild', () => { releaseBranch: 'release' } ); - expect( stubs.fetch.callCount ).to.equal( 1 ); - - const [ , options ] = stubs.fetch.firstCall.args; - - expect( options ).to.have.property( 'body' ); - - const body = JSON.parse( options.body ); - expect( body ).to.have.property( 'parameters' ); - expect( body.parameters ).to.have.property( 'isRelease', false ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledWith( + 'https://circleci.com/api/v2/project/github/ckeditor/ckeditor5-dev/pipeline', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Circle-Token': 'circle-token', + 'Content-Type': 'application/json' + }, + body: JSON.stringify( { + branch: 'master', + parameters: { + triggerCommitHash: 'abcd1234', + isRelease: false + } + } ) + } + ); } ); it( 'should include the "triggerRepositorySlug" parameter when passing the `releaseBranch` option', async () => { - stubs.fetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValue( { json: () => Promise.resolve( { error_message: null } ) @@ -129,19 +135,29 @@ describe( 'lib/triggerCircleBuild', () => { triggerRepositorySlug: 'ckeditor/ckeditor5' } ); - expect( stubs.fetch.callCount ).to.equal( 1 ); - - const [ , options ] = stubs.fetch.firstCall.args; - - expect( options ).to.have.property( 'body' ); - - const body = JSON.parse( options.body ); - expect( body ).to.have.property( 'parameters' ); - expect( body.parameters ).to.have.property( 'triggerRepositorySlug', 'ckeditor/ckeditor5' ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledWith( + 'https://circleci.com/api/v2/project/github/ckeditor/ckeditor5-dev/pipeline', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Circle-Token': 'circle-token', + 'Content-Type': 'application/json' + }, + body: JSON.stringify( { + branch: 'master', + parameters: { + triggerCommitHash: 'abcd1234', + triggerRepositorySlug: 'ckeditor/ckeditor5' + } + } ) + } + ); } ); it( 'should reject a promise when CircleCI responds with an error containing error_message property', async () => { - stubs.fetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValue( { json: () => Promise.resolve( { error_message: 'HTTP 404' } ) @@ -164,7 +180,7 @@ describe( 'lib/triggerCircleBuild', () => { } ); it( 'should reject a promise when CircleCI responds with an error containing message property', async () => { - stubs.fetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValue( { json: () => Promise.resolve( { message: 'HTTP 404' } ) diff --git a/packages/ckeditor5-dev-ci/tests/utils/get-job-approver.js b/packages/ckeditor5-dev-ci/tests/utils/get-job-approver.js index a54918660..bc7c1fbbf 100644 --- a/packages/ckeditor5-dev-ci/tests/utils/get-job-approver.js +++ b/packages/ckeditor5-dev-ci/tests/utils/get-job-approver.js @@ -3,69 +3,51 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +import { describe, expect, it, vi } from 'vitest'; +import getJobApprover from '../../lib/utils/get-job-approver.js'; describe( 'lib/utils/getJobApprover', () => { - let stubs, getJobApprover; - - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - fetch: sinon.stub() - }; - - mockery.registerMock( 'node-fetch', stubs.fetch ); - - getJobApprover = require( '../../lib/utils/get-job-approver' ); - } ); - - afterEach( () => { - mockery.disable(); - } ); - it( 'should return a GitHub login name of a user who approved a job in given workflow', async () => { - stubs.fetch.onCall( 0 ).resolves( { - json: () => Promise.resolve( { - items: [ - { name: 'job-1' }, - { name: 'job-2', approved_by: 'foo-unique-id' } - ] - } ) - } ); - - stubs.fetch.onCall( 1 ).resolves( { - json: () => Promise.resolve( { - login: 'foo' + const fetchMock = vi.fn(); + vi.stubGlobal( 'fetch', fetchMock ); + + fetchMock + .mockResolvedValueOnce( { + json: () => Promise.resolve( { + items: [ + { name: 'job-1' }, + { name: 'job-2', approved_by: 'foo-unique-id' } + ] + } ) } ) - } ); + .mockResolvedValueOnce( { + json: () => Promise.resolve( { + login: 'foo' + } ) + } ); const login = await getJobApprover( 'circle-token', 'abc-123-abc-456', 'job-2' ); expect( login ).to.equal( 'foo' ); - expect( stubs.fetch.callCount ).to.equal( 2 ); - - const [ firstUrl, firstOptions ] = stubs.fetch.firstCall.args; - - expect( firstUrl ).to.equal( 'https://circleci.com/api/v2/workflow/abc-123-abc-456/job' ); - expect( firstOptions ).to.have.property( 'method', 'get' ); - expect( firstOptions ).to.have.property( 'headers' ); - expect( firstOptions.headers ).to.have.property( 'Circle-Token', 'circle-token' ); - - const [ secondUrl, secondOptions ] = stubs.fetch.getCall( 1 ).args; - - expect( secondUrl ).to.equal( 'https://circleci.com/api/v2/user/foo-unique-id' ); - expect( secondOptions ).to.have.property( 'method', 'get' ); - expect( secondOptions ).to.have.property( 'headers' ); - expect( secondOptions.headers ).to.have.property( 'Circle-Token', 'circle-token' ); + expect( vi.mocked( fetchMock ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fetchMock ) ).toHaveBeenNthCalledWith( 1, + 'https://circleci.com/api/v2/workflow/abc-123-abc-456/job', + { + 'method': 'GET', + 'headers': expect.objectContaining( { + 'Circle-Token': 'circle-token' + } ) + } + ); + expect( vi.mocked( fetchMock ) ).toHaveBeenNthCalledWith( 2, + 'https://circleci.com/api/v2/user/foo-unique-id', + { + 'method': 'GET', + 'headers': expect.objectContaining( { + 'Circle-Token': 'circle-token' + } ) + } + ); } ); } ); diff --git a/packages/ckeditor5-dev-ci/vitest.config.js b/packages/ckeditor5-dev-ci/vitest.config.js new file mode 100644 index 000000000..5ad784a28 --- /dev/null +++ b/packages/ckeditor5-dev-ci/vitest.config.js @@ -0,0 +1,23 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig( { + test: { + testTimeout: 10000, + restoreMocks: true, + include: [ + 'tests/**/*.js' + ], + coverage: { + provider: 'v8', + include: [ + 'lib/**' + ], + reporter: [ 'text', 'json', 'html', 'lcov' ] + } + } +} ); diff --git a/packages/ckeditor5-dev-dependency-checker/bin/dependencychecker.js b/packages/ckeditor5-dev-dependency-checker/bin/dependencychecker.js index 5ea8a1556..dedd1bd4b 100755 --- a/packages/ckeditor5-dev-dependency-checker/bin/dependencychecker.js +++ b/packages/ckeditor5-dev-dependency-checker/bin/dependencychecker.js @@ -5,12 +5,10 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const minimist = require( 'minimist' ); -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); -const checkDependencies = require( '../lib/checkdependencies' ); +import path from 'path'; +import minimist from 'minimist'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import checkDependencies from '../lib/checkdependencies.js'; const { packagePaths, options } = parseArguments( process.argv.slice( 2 ) ); @@ -24,10 +22,10 @@ checkDependencies( packagePaths, options ) /** * Parses CLI arguments and options. * - * @param {Array.} args CLI arguments containing package paths and options. - * @returns {Object} result - * @returns {Set.} result.packagePaths Relative package paths. - * @returns {Object.} result.options Configuration options. + * @param {Array.} args CLI arguments containing package paths and options. + * @returns {object} result + * @returns {Set.} result.packagePaths Relative package paths. + * @returns {Object.} result.options Configuration options. */ function parseArguments( args ) { const config = { @@ -58,8 +56,8 @@ function parseArguments( args ) { * Returns relative (to the current work directory) paths to packages. If the provided `args` array is empty, * the packages will be read from the `packages/` directory. * - * @param {Array.} args CLI arguments with relative or absolute package paths. - * @returns {Set.} Relative package paths. + * @param {Array.} args CLI arguments with relative or absolute package paths. + * @returns {Set.} Relative package paths. */ function getPackagePaths( args ) { if ( !args.length ) { diff --git a/packages/ckeditor5-dev-dependency-checker/lib/checkdependencies.js b/packages/ckeditor5-dev-dependency-checker/lib/checkdependencies.js index 810a16141..abe80870c 100644 --- a/packages/ckeditor5-dev-dependency-checker/lib/checkdependencies.js +++ b/packages/ckeditor5-dev-dependency-checker/lib/checkdependencies.js @@ -3,23 +3,21 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const upath = require( 'upath' ); -const { globSync } = require( 'glob' ); -const depCheck = require( 'depcheck' ); -const chalk = require( 'chalk' ); +import fs from 'fs-extra'; +import upath from 'upath'; +import { globSync } from 'glob'; +import depCheck from 'depcheck'; +import chalk from 'chalk'; /** * Checks dependencies sequentially in all provided packages. * - * @param {Set.} packagePaths Relative paths to packages. - * @param {Object} options Options. - * @param {Boolean} [options.quiet=false] Whether to inform about the progress. - * @returns {Promise.} Resolves a promise with a flag informing whether detected an error. + * @param {Set.} packagePaths Relative paths to packages. + * @param {object} options Options. + * @param {boolean} [options.quiet=false] Whether to inform about the progress. + * @returns {Promise.} Resolves a promise with a flag informing whether detected an error. */ -module.exports = async function checkDependencies( packagePaths, options ) { +export default async function checkDependencies( packagePaths, options ) { let foundError = false; for ( const packagePath of packagePaths ) { @@ -34,15 +32,15 @@ module.exports = async function checkDependencies( packagePaths, options ) { } return Promise.resolve( foundError ); -}; +} /** * Checks dependencies in provided package. If the folder does not contain a package.json file the function quits with success. * - * @param {String} packagePath Relative path to package. - * @param {Object} options Options. - * @param {Boolean} [options.quiet=false] Whether to inform about the progress. - * @returns {Promise.} The result of checking the dependencies in the package: true = no errors found. + * @param {string} packagePath Relative path to package. + * @param {object} options Options. + * @param {boolean} [options.quiet=false] Whether to inform about the progress. + * @returns {Promise.} The result of checking the dependencies in the package: true = no errors found. */ async function checkDependenciesInPackage( packagePath, options ) { const packageAbsolutePath = upath.resolve( packagePath ); @@ -54,7 +52,7 @@ async function checkDependenciesInPackage( packagePath, options ) { return true; } - const packageJson = require( packageJsonPath ); + const packageJson = await fs.readJson( packageJsonPath ); const missingCSSFiles = []; const onMissingCSSFile = file => missingCSSFiles.push( file ); @@ -64,6 +62,7 @@ async function checkDependenciesInPackage( packagePath, options ) { parsers: { '**/*.css': filePath => parsePostCSS( filePath, onMissingCSSFile ), '**/*.cjs': depCheck.parser.es6, + '**/*.mjs': depCheck.parser.es6, '**/*.js': depCheck.parser.es6, '**/*.jsx': depCheck.parser.jsx, '**/*.ts': depCheck.parser.typescript, @@ -162,10 +161,10 @@ async function checkDependenciesInPackage( packagePath, options ) { * Returns an array that contains list of files that import modules using full package name instead of relative path. * * @param repositoryPath An absolute path to the directory which should be checked. - * @returns {Array.} + * @returns {Array.} */ function getInvalidItselfImports( repositoryPath ) { - const packageJson = require( upath.join( repositoryPath, 'package.json' ) ); + const packageJson = fs.readJsonSync( upath.join( repositoryPath, 'package.json' ) ); const globPattern = upath.join( repositoryPath, '@(src|tests)/**/*.js' ); const invalidImportsItself = new Set(); @@ -194,9 +193,9 @@ function getInvalidItselfImports( repositoryPath ) { /** * Groups missing dependencies returned by `depcheck` as `dependencies` or `devDependencies`. * - * @param {Object} missingPackages The `missing` value from object returned by `depcheck`. - * @param {String} currentPackage Name of current package. - * @returns {Promise.>>} + * @param {object} missingPackages The `missing` value from object returned by `depcheck`. + * @param {string} currentPackage Name of current package. + * @returns {Promise.>>} */ async function groupMissingPackages( missingPackages, currentPackage ) { delete missingPackages[ currentPackage ]; @@ -221,9 +220,9 @@ async function groupMissingPackages( missingPackages, currentPackage ) { * Checks whether all packages that have been imported by the CSS file are defined in `package.json` as `dependencies`. * Returned array contains list of used packages. * - * @param {String} filePath An absolute path to the checking file. - * @param {Function} onMissingCSSFile Error handler called when a CSS file is not found. - * @returns {Array.|undefined} + * @param {string} filePath An absolute path to the checking file. + * @param {function} onMissingCSSFile Error handler called when a CSS file is not found. + * @returns {Array.|undefined} */ function parsePostCSS( filePath, onMissingCSSFile ) { const fileContent = fs.readFileSync( filePath, 'utf-8' ); @@ -289,9 +288,9 @@ function parsePostCSS( filePath, onMissingCSSFile ) { * Checks whether packages specified as `devDependencies` are not duplicated with items defined as `dependencies`. * * @see https://github.com/ckeditor/ckeditor5/issues/7706#issuecomment-665569410 - * @param {Object|undefined} dependencies - * @param {Object|undefined} devDependencies - * @returns {Array.} + * @param {object|undefined} dependencies + * @param {object|undefined} devDependencies + * @returns {Array.} */ function findDuplicatedDependencies( dependencies, devDependencies ) { const deps = Object.keys( dependencies || {} ); @@ -319,11 +318,11 @@ function findDuplicatedDependencies( dependencies, devDependencies ) { * verifies wrongly placed ones. * * @see https://github.com/ckeditor/ckeditor5/issues/8817#issuecomment-759353134 - * @param {Object|undefined} options.dependencies Defined dependencies from package.json. - * @param {Object|undefined} options.devDependencies Defined development dependencies from package.json. - * @param {Object} options.dependenciesToCheck All dependencies that have been found and files where they are used. - * @param {Array} options.dependenciesToIgnore An array of package names that should not be checked. - * @returns {Promise.>} Misplaced packages. Each array item is an object containing + * @param {object|undefined} options.dependencies Defined dependencies from package.json. + * @param {object|undefined} options.devDependencies Defined development dependencies from package.json. + * @param {object} options.dependenciesToCheck All dependencies that have been found and files where they are used. + * @param {Array.} options.dependenciesToIgnore An array of package names that should not be checked. + * @returns {Promise.>} Misplaced packages. Each array item is an object containing * the `description` string and `packageNames` array of strings. */ async function findMisplacedDependencies( options ) { @@ -373,9 +372,9 @@ async function findMisplacedDependencies( options ) { * Checks if a given package is a development-only dependency. Package is considered a dev dependency * if it is used only in files that are not used in the final build, such as tests, demos or typings. * - * @param {String} packageName - * @param {Array.} absolutePaths Files where a given package has been imported. - * @returns {Promise.} + * @param {string} packageName + * @param {Array.} absolutePaths Files where a given package has been imported. + * @returns {Promise.} */ async function isDevDependency( packageName, absolutePaths ) { if ( packageName.startsWith( '@types/' ) ) { @@ -432,9 +431,9 @@ async function isDevDependency( packageName, absolutePaths ) { /** * Parses TS file from `absolutePath` and returns a list of import and export types from `packageName`. * - * @param {String} packageName - * @param {String} absolutePath File where a given package has been imported. - * @returns {Promise.>} Array of import kinds. + * @param {string} packageName + * @param {string} absolutePath File where a given package has been imported. + * @returns {Promise.>} Array of import kinds. */ async function getImportAndExportKinds( packageName, absolutePath ) { const astContent = await depCheck.parser.typescript( absolutePath ); @@ -458,7 +457,7 @@ async function getImportAndExportKinds( packageName, absolutePath ) { /** * Displays all found errors. * - * @param {Array.} data Collection of errors. + * @param {Array.} data Collection of errors. */ function showErrors( data ) { if ( data[ 0 ] ) { diff --git a/packages/ckeditor5-dev-dependency-checker/lib/index.js b/packages/ckeditor5-dev-dependency-checker/lib/index.js index 5b097fdda..6d0a89b7b 100644 --- a/packages/ckeditor5-dev-dependency-checker/lib/index.js +++ b/packages/ckeditor5-dev-dependency-checker/lib/index.js @@ -3,10 +3,4 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const checkDependencies = require( './checkdependencies' ); - -module.exports = { - checkDependencies -}; +export { default as checkDependencies } from './checkdependencies.js'; diff --git a/packages/ckeditor5-dev-dependency-checker/package.json b/packages/ckeditor5-dev-dependency-checker/package.json index 2311782cd..1c183a12b 100644 --- a/packages/ckeditor5-dev-dependency-checker/package.json +++ b/packages/ckeditor5-dev-dependency-checker/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-dependency-checker", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "Contains tools for validating dependencies specified in package.json.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -16,6 +16,7 @@ "node": ">=18.0.0", "npm": ">=5.7.1" }, + "type": "module", "files": [ "bin", "lib" @@ -24,10 +25,11 @@ "ckeditor5-dev-dependency-checker": "bin/dependencychecker.js" }, "dependencies": { - "@ckeditor/ckeditor5-dev-utils": "^43.0.0", - "chalk": "^4.1.0", + "@ckeditor/ckeditor5-dev-utils": "^44.0.0-alpha.5", + "chalk": "^5.0.0", "depcheck": "^1.3.1", - "glob": "^10.2.5", + "fs-extra": "^11.0.0", + "glob": "^10.0.0", "minimist": "^1.2.8", "upath": "^2.0.1" } diff --git a/packages/ckeditor5-dev-docs/lib/build.js b/packages/ckeditor5-dev-docs/lib/build.js index 2249bd0d9..e54f406a1 100644 --- a/packages/ckeditor5-dev-docs/lib/build.js +++ b/packages/ckeditor5-dev-docs/lib/build.js @@ -3,70 +3,120 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { glob } from 'glob'; +import TypeDoc from 'typedoc'; +import typedocPlugins from '@ckeditor/typedoc-plugins'; + +import validators from './validators/index.js'; /** - * Builds CKEditor 5 documentation. + * Builds CKEditor 5 documentation using `typedoc`. * - * @param {JSDocConfig|TypedocConfig} config + * @param {TypedocConfig} config * @returns {Promise} */ -module.exports = async function build( config ) { - const type = config.type || 'jsdoc'; - - if ( type === 'jsdoc' ) { - return require( './buildjsdoc' )( config ); - } else if ( type === 'typedoc' ) { - return require( './buildtypedoc' )( config ); - } else { - throw new Error( `Unknown documentation tool (${ type }).` ); +export default async function build( config ) { + const { plugins } = typedocPlugins; + const sourceFilePatterns = config.sourceFiles.filter( Boolean ); + const strictMode = config.strict || false; + const extraPlugins = config.extraPlugins || []; + const ignoreFiles = config.ignoreFiles || []; + const validatorOptions = config.validatorOptions || {}; + + const files = await glob( sourceFilePatterns, { + ignore: ignoreFiles + } ); + const typeDoc = new TypeDoc.Application(); + + typeDoc.options.addReader( new TypeDoc.TSConfigReader() ); + typeDoc.options.addReader( new TypeDoc.TypeDocReader() ); + + typeDoc.bootstrap( { + tsconfig: config.tsconfig, + excludeExternals: true, + entryPoints: files, + logLevel: 'Warn', + basePath: config.cwd, + blockTags: [ + '@eventName', + '@default' + ], + inlineTags: [ + '@link', + '@glink' + ], + modifierTags: [ + '@publicApi', + '@skipSource', + '@internal' + ], + plugin: [ + // Fixes `"name": 'default" in the output project. + 'typedoc-plugin-rename-defaults', + + plugins[ 'typedoc-plugin-module-fixer' ], + plugins[ 'typedoc-plugin-symbol-fixer' ], + plugins[ 'typedoc-plugin-interface-augmentation-fixer' ], + plugins[ 'typedoc-plugin-tag-error' ], + plugins[ 'typedoc-plugin-tag-event' ], + plugins[ 'typedoc-plugin-tag-observable' ], + plugins[ 'typedoc-plugin-purge-private-api-docs' ], + + // The `event-inheritance-fixer` plugin must be loaded after `tag-event` plugin, as it depends on its output. + plugins[ 'typedoc-plugin-event-inheritance-fixer' ], + + // The `event-param-fixer` plugin must be loaded after `tag-event` and `tag-observable` plugins, as it depends on their output. + plugins[ 'typedoc-plugin-event-param-fixer' ], + + ...extraPlugins + ] + } ); + + console.log( 'Typedoc started...' ); + + const conversionResult = typeDoc.convert(); + + if ( !conversionResult ) { + throw 'Something went wrong with TypeDoc.'; } -}; -/** - * @typedef {Object} JSDocConfig - * - * @property {'jsdoc'} type - * - * @property {Array.} sourceFiles Glob pattern with source files. - * - * @property {String} readmePath Path to `README.md`. - * - * @property {Boolean} [validateOnly=false] Whether JSDoc should only validate the documentation and finish - * with error code `1`. If not passed, the errors will be printed to the console but the task will finish with `0`. - * - * @property {Boolean} [strict=false] If `true`, errors found during the validation will finish the process - * and exit code will be changed to `1`. - * - * @property {String} [outputPath='docs/api/output.json'] A path to the place where extracted doclets will be saved. - * - * @property {String} [extraPlugins=[]] An array of path to extra plugins that will be added to JSDoc. - */ + const validationResult = validators.validate( conversionResult, typeDoc, validatorOptions ); + + if ( !validationResult && strictMode ) { + throw 'Something went wrong with TypeDoc.'; + } + + if ( config.outputPath ) { + await typeDoc.generateJson( conversionResult, config.outputPath ); + } + + console.log( `Documented ${ files.length } files!` ); +} /** - * @typedef {Object} TypedocConfig + * @typedef {object} TypedocConfig * - * @property {'typedoc'} type + * @property {object} config * - * @property {Object} config + * @property {string} cwd * - * @property {String} cwd + * @property {string} tsconfig * - * @property {String} tsconfig + * @property {Array.} sourceFiles Glob pattern with source files. * - * @property {Array.} sourceFiles Glob pattern with source files. + * @property {Array.} [ignoreFiles=[]] Glob pattern with files to ignore. * - * @property {Boolean} [strict=false] If `true`, errors found during the validation will finish the process + * @property {boolean} [strict=false] If `true`, errors found during the validation will finish the process * and exit code will be changed to `1`. - * @property {String} [outputPath] A path to the place where extracted doclets will be saved. Is an optional value due to tests. + * @property {string} [outputPath] A path to the place where extracted doclets will be saved. Is an optional value due to tests. * - * @property {String} [extraPlugins=[]] An array of path to extra plugins that will be added to Typedoc. + * @property {string} [extraPlugins=[]] An array of path to extra plugins that will be added to Typedoc. * * @property {TypedocValidator} [validatorOptions={}] An optional configuration object for validator. */ /** - * @typedef {Object} TypedocValidator + * @typedef {object} TypedocValidator * - * @property {Boolean} [enableOverloadValidator=false] If set to `true`, the overloads validator will be enabled. + * @property {boolean} [enableOverloadValidator=false] If set to `true`, the overloads validator will be enabled. */ diff --git a/packages/ckeditor5-dev-docs/lib/buildjsdoc.js b/packages/ckeditor5-dev-docs/lib/buildjsdoc.js deleted file mode 100644 index 8c657751e..000000000 --- a/packages/ckeditor5-dev-docs/lib/buildjsdoc.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const fs = require( 'fs-extra' ); -const tmp = require( 'tmp' ); -const glob = require( 'fast-glob' ); -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); - -/** - * Builds CKEditor 5 documentation using `jsdoc`. - * - * @param {JSDocConfig} config - * @returns {Promise} - */ -module.exports = async function build( config ) { - const sourceFilePatterns = [ - config.readmePath, - ...config.sourceFiles - ]; - - const extraPlugins = config.extraPlugins || []; - const outputPath = config.outputPath || 'docs/api/output.json'; - const validateOnly = config.validateOnly || false; - const strictCheck = config.strict || false; - - // Pass options to plugins via env variables. - // Since plugins are added using `require` calls other forms are currently impossible. - process.env.JSDOC_OUTPUT_PATH = outputPath; - - if ( validateOnly ) { - process.env.JSDOC_VALIDATE_ONLY = 'true'; - } - - if ( strictCheck ) { - process.env.JSDOC_STRICT_CHECK = 'true'; - } - - const files = await glob( sourceFilePatterns ); - - const jsDocConfig = { - plugins: [ - require.resolve( 'jsdoc/plugins/markdown' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/purge-private-api-docs' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/export-fixer/export-fixer' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/custom-tags/error' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/custom-tags/observable' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/observable-event-provider' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/longname-fixer/longname-fixer' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/fix-code-snippets' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/relation-fixer' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/event-extender/event-extender' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/cleanup' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/validator/validator' ), - require.resolve( '@ckeditor/jsdoc-plugins/lib/utils/doclet-logger' ), - ...extraPlugins - ], - source: { - include: files - }, - opts: { - encoding: 'utf8', - recurse: true, - access: 'all', - template: 'templates/silent' - } - }; - - const tmpConfig = tmp.fileSync(); - - await fs.writeFile( tmpConfig.name, JSON.stringify( jsDocConfig ) ); - - console.log( 'JSDoc started...' ); - - try { - // Not so beautiful API as for 2020... - // See more in https://github.com/jsdoc/jsdoc/issues/938. - const cmd = require.resolve( 'jsdoc/jsdoc.js' ); - - // The `node` command is used for explicitly needed for Windows. - // See https://github.com/ckeditor/ckeditor5/issues/7212. - tools.shExec( `node ${ cmd } -c ${ tmpConfig.name }`, { verbosity: 'info' } ); - } catch ( error ) { - console.error( 'An error was thrown by JSDoc:' ); - - throw error; - } - - console.log( `Documented ${ files.length } files!` ); -}; diff --git a/packages/ckeditor5-dev-docs/lib/buildtypedoc.js b/packages/ckeditor5-dev-docs/lib/buildtypedoc.js deleted file mode 100644 index e830979e6..000000000 --- a/packages/ckeditor5-dev-docs/lib/buildtypedoc.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const glob = require( 'fast-glob' ); -const TypeDoc = require( 'typedoc' ); -const { plugins } = require( '@ckeditor/typedoc-plugins' ); - -const validators = require( './validators' ); - -/** - * Builds CKEditor 5 documentation using `typedoc`. - * - * @param {TypedocConfig} config - * @returns {Promise} - */ -module.exports = async function build( config ) { - const sourceFilePatterns = config.sourceFiles.filter( Boolean ); - const strictMode = config.strict || false; - const extraPlugins = config.extraPlugins || []; - const validatorOptions = config.validatorOptions || {}; - - const files = await glob( sourceFilePatterns ); - const typeDoc = new TypeDoc.Application(); - - typeDoc.options.addReader( new TypeDoc.TSConfigReader() ); - typeDoc.options.addReader( new TypeDoc.TypeDocReader() ); - - typeDoc.bootstrap( { - tsconfig: config.tsconfig, - excludeExternals: true, - entryPoints: files, - logLevel: 'Warn', - basePath: config.cwd, - blockTags: [ - '@eventName', - '@default' - ], - inlineTags: [ - '@link', - '@glink' - ], - modifierTags: [ - '@publicApi', - '@skipSource', - '@internal' - ], - plugin: [ - // Fixes `"name": 'default" in the output project. - 'typedoc-plugin-rename-defaults', - - plugins[ 'typedoc-plugin-module-fixer' ], - plugins[ 'typedoc-plugin-symbol-fixer' ], - plugins[ 'typedoc-plugin-interface-augmentation-fixer' ], - plugins[ 'typedoc-plugin-tag-error' ], - plugins[ 'typedoc-plugin-tag-event' ], - plugins[ 'typedoc-plugin-tag-observable' ], - plugins[ 'typedoc-plugin-purge-private-api-docs' ], - - // The `event-inheritance-fixer` plugin must be loaded after `tag-event` plugin, as it depends on its output. - plugins[ 'typedoc-plugin-event-inheritance-fixer' ], - - // The `event-param-fixer` plugin must be loaded after `tag-event` and `tag-observable` plugins, as it depends on their output. - plugins[ 'typedoc-plugin-event-param-fixer' ], - - ...extraPlugins - ] - } ); - - console.log( 'Typedoc started...' ); - - const conversionResult = typeDoc.convert(); - - if ( !conversionResult ) { - throw 'Something went wrong with TypeDoc.'; - } - - const validationResult = validators.validate( conversionResult, typeDoc, validatorOptions ); - - if ( !validationResult && strictMode ) { - throw 'Something went wrong with TypeDoc.'; - } - - if ( config.outputPath ) { - await typeDoc.generateJson( conversionResult, config.outputPath ); - } - - console.log( `Documented ${ files.length } files!` ); -}; diff --git a/packages/ckeditor5-dev-docs/lib/index.js b/packages/ckeditor5-dev-docs/lib/index.js index abe4f686e..d3947d7ce 100644 --- a/packages/ckeditor5-dev-docs/lib/index.js +++ b/packages/ckeditor5-dev-docs/lib/index.js @@ -3,10 +3,4 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const build = require( './build' ); - -module.exports = { - build -}; +export { default as build } from './build.js'; diff --git a/packages/ckeditor5-dev-docs/lib/validators/fires-validator/index.js b/packages/ckeditor5-dev-docs/lib/validators/fires-validator/index.js index 125eb1e46..ab11d66e6 100644 --- a/packages/ckeditor5-dev-docs/lib/validators/fires-validator/index.js +++ b/packages/ckeditor5-dev-docs/lib/validators/fires-validator/index.js @@ -3,20 +3,19 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { ReflectionKind } = require( 'typedoc' ); -const { utils } = require( '@ckeditor/typedoc-plugins' ); +import { ReflectionKind } from 'typedoc'; +import typedocPlugins from '@ckeditor/typedoc-plugins'; /** * Validates the output produced by TypeDoc. * * It checks if the event in the "@fires" tag exists. * - * @param {Object} project Generated output from TypeDoc to validate. - * @param {Function} onError A callback that is executed when a validation error is detected. + * @param {object} project Generated output from TypeDoc to validate. + * @param {function} onError A callback that is executed when a validation error is detected. */ -module.exports = function validate( project, onError ) { +export default function validate( project, onError ) { + const { utils } = typedocPlugins; const reflections = project .getReflectionsByKind( ReflectionKind.Class | ReflectionKind.CallSignature ) .filter( utils.isReflectionValid ); @@ -38,9 +37,11 @@ module.exports = function validate( project, onError ) { } } } -}; +} function getIdentifiersFromFiresTag( reflection ) { + const { utils } = typedocPlugins; + if ( !reflection.comment ) { return []; } diff --git a/packages/ckeditor5-dev-docs/lib/validators/index.js b/packages/ckeditor5-dev-docs/lib/validators/index.js index 315313e9a..71a14bb63 100644 --- a/packages/ckeditor5-dev-docs/lib/validators/index.js +++ b/packages/ckeditor5-dev-docs/lib/validators/index.js @@ -3,25 +3,24 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { utils } = require( '@ckeditor/typedoc-plugins' ); -const seeValidator = require( './see-validator' ); -const linkValidator = require( './link-validator' ); -const firesValidator = require( './fires-validator' ); -const moduleValidator = require( './module-validator' ); -const overloadsValidator = require( './overloads-validator' ); +import typedocPlugins from '@ckeditor/typedoc-plugins'; +import seeValidator from './see-validator/index.js'; +import linkValidator from './link-validator/index.js'; +import firesValidator from './fires-validator/index.js'; +import moduleValidator from './module-validator/index.js'; +import overloadsValidator from './overloads-validator/index.js'; /** * Validates the CKEditor 5 documentation. * - * @param {Object} project Generated output from TypeDoc to validate. - * @param {Object} typeDoc A TypeDoc application instance. + * @param {object} project Generated output from TypeDoc to validate. + * @param {object} typeDoc A TypeDoc application instance. * @param {TypedocValidator} [options={}] A configuration object. - * @returns {Boolean} + * @returns {boolean} */ -module.exports = { +export default { validate( project, typeDoc, options = {} ) { + const { utils } = typedocPlugins; const validators = [ seeValidator, linkValidator, diff --git a/packages/ckeditor5-dev-docs/lib/validators/link-validator/index.js b/packages/ckeditor5-dev-docs/lib/validators/link-validator/index.js index 0c7dd4692..fb0805036 100644 --- a/packages/ckeditor5-dev-docs/lib/validators/link-validator/index.js +++ b/packages/ckeditor5-dev-docs/lib/validators/link-validator/index.js @@ -3,20 +3,19 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { ReflectionKind } = require( 'typedoc' ); -const { utils } = require( '@ckeditor/typedoc-plugins' ); +import { ReflectionKind } from 'typedoc'; +import typedocPlugins from '@ckeditor/typedoc-plugins'; /** * Validates the output produced by TypeDoc. * * It checks if the identifier in the "@link" tag points to an existing doclet. * - * @param {Object} project Generated output from TypeDoc to validate. - * @param {Function} onError A callback that is executed when a validation error is detected. + * @param {object} project Generated output from TypeDoc to validate. + * @param {function} onError A callback that is executed when a validation error is detected. */ -module.exports = function validate( project, onError ) { +export default function validate( project, onError ) { + const { utils } = typedocPlugins; const reflections = project.getReflectionsByKind( ReflectionKind.All ).filter( utils.isReflectionValid ); for ( const reflection of reflections ) { @@ -34,7 +33,7 @@ module.exports = function validate( project, onError ) { } } } -}; +} function getIdentifiersFromLinkTag( reflection ) { if ( !reflection.comment ) { diff --git a/packages/ckeditor5-dev-docs/lib/validators/module-validator/index.js b/packages/ckeditor5-dev-docs/lib/validators/module-validator/index.js index 4f13f1cc4..a9fd913d4 100644 --- a/packages/ckeditor5-dev-docs/lib/validators/module-validator/index.js +++ b/packages/ckeditor5-dev-docs/lib/validators/module-validator/index.js @@ -3,10 +3,8 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { ReflectionKind } = require( 'typedoc' ); -const { utils } = require( '@ckeditor/typedoc-plugins' ); +import { ReflectionKind } from 'typedoc'; +import typedocPlugins from '@ckeditor/typedoc-plugins'; const AUGMENTATION_MODULE_REGEXP = /[^\\/]+[\\/]src[\\/]augmentation/; @@ -15,10 +13,11 @@ const AUGMENTATION_MODULE_REGEXP = /[^\\/]+[\\/]src[\\/]augmentation/; * * It checks if the module name matches the path to the file where the module is defined. * - * @param {Object} project Generated output from TypeDoc to validate. - * @param {Function} onError A callback that is executed when a validation error is detected. + * @param {object} project Generated output from TypeDoc to validate. + * @param {function} onError A callback that is executed when a validation error is detected. */ -module.exports = function validate( project, onError ) { +export default function validate( project, onError ) { + const { utils } = typedocPlugins; const reflections = project.getReflectionsByKind( ReflectionKind.Module ); for ( const reflection of reflections ) { @@ -53,4 +52,4 @@ module.exports = function validate( project, onError ) { onError( `Invalid module name: "${ reflection.name }"`, reflection ); } } -}; +} diff --git a/packages/ckeditor5-dev-docs/lib/validators/overloads-validator/index.js b/packages/ckeditor5-dev-docs/lib/validators/overloads-validator/index.js index d4c00537e..b56fdcfa2 100644 --- a/packages/ckeditor5-dev-docs/lib/validators/overloads-validator/index.js +++ b/packages/ckeditor5-dev-docs/lib/validators/overloads-validator/index.js @@ -3,10 +3,8 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { ReflectionKind } = require( 'typedoc' ); -const { utils } = require( '@ckeditor/typedoc-plugins' ); +import { ReflectionKind } from 'typedoc'; +import typedocPlugins from '@ckeditor/typedoc-plugins'; // The `@label` validator is currently not used. // See: https://github.com/ckeditor/ckeditor5/issues/13591. @@ -18,10 +16,11 @@ const { utils } = require( '@ckeditor/typedoc-plugins' ); * * Also, it prevents using the same name twice for overloaded structures. * - * @param {Object} project Generated output from TypeDoc to validate. - * @param {Function} onError A callback that is executed when a validation error is detected. + * @param {object} project Generated output from TypeDoc to validate. + * @param {function} onError A callback that is executed when a validation error is detected. */ -module.exports = function validate( project, onError ) { +export default function validate( project, onError ) { + const { utils } = typedocPlugins; const kinds = ReflectionKind.Method | ReflectionKind.Constructor | ReflectionKind.Function; const reflections = project.getReflectionsByKind( kinds ).filter( utils.isReflectionValid ); @@ -52,4 +51,4 @@ module.exports = function validate( project, onError ) { } } } -}; +} diff --git a/packages/ckeditor5-dev-docs/lib/validators/see-validator/index.js b/packages/ckeditor5-dev-docs/lib/validators/see-validator/index.js index a86661ab7..88d090c15 100644 --- a/packages/ckeditor5-dev-docs/lib/validators/see-validator/index.js +++ b/packages/ckeditor5-dev-docs/lib/validators/see-validator/index.js @@ -3,20 +3,19 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { ReflectionKind } = require( 'typedoc' ); -const { utils } = require( '@ckeditor/typedoc-plugins' ); +import { ReflectionKind } from 'typedoc'; +import typedocPlugins from '@ckeditor/typedoc-plugins'; /** * Validates the output produced by TypeDoc. * * It checks if the identifier in the "@see" tag points to an existing doclet. * - * @param {Object} project Generated output from TypeDoc to validate. - * @param {Function} onError A callback that is executed when a validation error is detected. + * @param {object} project Generated output from TypeDoc to validate. + * @param {function} onError A callback that is executed when a validation error is detected. */ -module.exports = function validate( project, onError ) { +export default function validate( project, onError ) { + const { utils } = typedocPlugins; const reflections = project.getReflectionsByKind( ReflectionKind.All ).filter( utils.isReflectionValid ); for ( const reflection of reflections ) { @@ -34,7 +33,7 @@ module.exports = function validate( project, onError ) { } } } -}; +} function getIdentifiersFromSeeTag( reflection ) { if ( !reflection.comment ) { diff --git a/packages/ckeditor5-dev-docs/package.json b/packages/ckeditor5-dev-docs/package.json index 046c4e8ef..035147e52 100644 --- a/packages/ckeditor5-dev-docs/package.json +++ b/packages/ckeditor5-dev-docs/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-docs", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "Tasks used to build and verify the documentation for CKEditor 5.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -16,31 +16,26 @@ "node": ">=18.0.0", "npm": ">=5.7.1" }, + "type": "module", "main": "lib/index.js", "files": [ "lib" ], "dependencies": { - "@ckeditor/ckeditor5-dev-utils": "^43.0.0", - "@ckeditor/jsdoc-plugins": "^43.0.0", - "@ckeditor/typedoc-plugins": "^43.0.0", - "fast-glob": "^3.2.4", - "fs-extra": "^11.2.0", - "jsdoc": "ckeditor/jsdoc#fixed-trailing-comment-doclets", + "@ckeditor/ckeditor5-dev-utils": "^44.0.0-alpha.5", + "@ckeditor/typedoc-plugins": "^44.0.0-alpha.5", + "glob": "^10.0.0", + "fs-extra": "^11.0.0", "tmp": "^0.2.1", "typedoc": "^0.23.15", "typedoc-plugin-rename-defaults": "0.6.6" }, "devDependencies": { - "chai": "^4.2.0", - "mocha": "^7.1.2", - "proxyquire": "^2.1.3", - "sinon": "^9.2.4", - "sinon-chai": "^3.7.0" + "vitest": "^2.0.5" }, "scripts": { - "test": "mocha './tests/**/*.js' --timeout 10000", - "coverage": "nyc --reporter=lcov --reporter=text-summary yarn run test" + "test": "vitest run --config vitest.config.js", + "coverage": "vitest run --config vitest.config.js --coverage" }, "depcheckIgnore": [ "typedoc-plugin-rename-defaults" diff --git a/packages/ckeditor5-dev-docs/tests/utils.js b/packages/ckeditor5-dev-docs/tests/_utils.js similarity index 82% rename from packages/ckeditor5-dev-docs/tests/utils.js rename to packages/ckeditor5-dev-docs/tests/_utils.js index 537be756f..99ffec142 100644 --- a/packages/ckeditor5-dev-docs/tests/utils.js +++ b/packages/ckeditor5-dev-docs/tests/_utils.js @@ -3,13 +3,11 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /** * Replaces Windows style paths to Unix. * * @param value - * @returns {String} + * @returns {string} */ function normalizePath( ...value ) { return value.join( '/' ).replace( /\\/g, '/' ); @@ -18,8 +16,8 @@ function normalizePath( ...value ) { /** * Returns the source file path with line number from a reflection. * - * @param {require('typedoc').Reflection} reflection - * @returns {String} + * @param {import('typedoc').Reflection} reflection + * @returns {string} */ function getSource( reflection ) { if ( reflection.sources ) { @@ -31,7 +29,7 @@ function getSource( reflection ) { return getSource( reflection.parent ); } -module.exports = { +export default { normalizePath, getSource }; diff --git a/packages/ckeditor5-dev-docs/tests/validators/fires-validator/index.js b/packages/ckeditor5-dev-docs/tests/validators/fires-validator/index.js index 17f3428b1..c80cfd7cc 100644 --- a/packages/ckeditor5-dev-docs/tests/validators/fires-validator/index.js +++ b/packages/ckeditor5-dev-docs/tests/validators/fires-validator/index.js @@ -3,49 +3,48 @@ * For licensing, see LICENSE.md. */ -const chai = require( 'chai' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); -const testUtils = require( '../../utils' ); +import { describe, it, expect, vi } from 'vitest'; +import { fileURLToPath } from 'url'; +import path from 'path'; +import testUtils from '../../_utils.js'; +import build from '../../../lib/build.js'; -const { expect } = chai; -chai.use( require( 'sinon-chai' ) ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); -describe( 'dev-docs/validators/fires-validator', function() { - this.timeout( 10 * 1000 ); - - const FIXTURES_PATH = testUtils.normalizePath( __dirname, 'fixtures' ); - const SOURCE_FILES = testUtils.normalizePath( FIXTURES_PATH, '**', '*.ts' ); - const TSCONFIG_PATH = testUtils.normalizePath( FIXTURES_PATH, 'tsconfig.json' ); +const stubs = vi.hoisted( () => { + return { + onErrorCallback: vi.fn() + }; +} ); - const onErrorCallback = sinon.stub(); +vi.stubGlobal( 'console', { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn() +} ); - before( async () => { - const validators = proxyquire( '../../../lib/validators', { - './fires-validator': project => { - return require( '../../../lib/validators/fires-validator' )( project, onErrorCallback ); - }, - './module-validator': sinon.spy() - } ); +vi.mock( '../../../lib/validators/fires-validator', async () => { + const { default: validator } = await vi.importActual( '../../../lib/validators/fires-validator' ); - const build = proxyquire( '../../../lib/buildtypedoc', { - './validators': validators - } ); + return { + default: project => validator( project, ( ...args ) => stubs.onErrorCallback( ...args ) ) + }; +} ); - const logStub = sinon.stub( console, 'log' ); +describe( 'dev-docs/validators/fires-validator', function() { + const FIXTURES_PATH = testUtils.normalizePath( __dirname, 'fixtures' ); + const SOURCE_FILES = testUtils.normalizePath( FIXTURES_PATH, '**', '*.ts' ); + const TSCONFIG_PATH = testUtils.normalizePath( FIXTURES_PATH, 'tsconfig.json' ); + it( 'should warn if fired event does not exist', async () => { await build( { - type: 'typedoc', cwd: FIXTURES_PATH, tsconfig: TSCONFIG_PATH, sourceFiles: [ SOURCE_FILES ], strict: false } ); - logStub.restore(); - } ); - - it( 'should warn if fired event does not exist', () => { const expectedErrors = [ { identifier: 'event-non-existing', @@ -81,13 +80,24 @@ describe( 'dev-docs/validators/fires-validator', function() { } ]; - expect( onErrorCallback.callCount ).to.equal( expectedErrors.length ); + expect( stubs.onErrorCallback ).toHaveBeenCalledTimes( expectedErrors.length ); + + for ( const call of stubs.onErrorCallback.mock.calls ) { + expect( call ).toSatisfy( call => { + const [ message, reflection ] = call; + + return expectedErrors.some( error => { + if ( message !== `Incorrect event name: "${ error.identifier }" in the @fires tag` ) { + return false; + } + + if ( testUtils.getSource( reflection ) !== error.source ) { + return false; + } - for ( const error of expectedErrors ) { - expect( onErrorCallback ).to.be.calledWith( - `Incorrect event name: "${ error.identifier }" in the @fires tag`, - sinon.match( reflection => error.source === testUtils.getSource( reflection ) ) - ); + return true; + } ); + } ); } } ); } ); diff --git a/packages/ckeditor5-dev-docs/tests/validators/link-validator/fixtures/inheritance/derivedclass.ts b/packages/ckeditor5-dev-docs/tests/validators/link-validator/fixtures/inheritance/derivedclass.ts index bdfd0c730..4831fa2ee 100644 --- a/packages/ckeditor5-dev-docs/tests/validators/link-validator/fixtures/inheritance/derivedclass.ts +++ b/packages/ckeditor5-dev-docs/tests/validators/link-validator/fixtures/inheritance/derivedclass.ts @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import BaseCLass from './baseclass'; +import BaseCLass from './baseclass.js'; /** * @module fixtures/inheritance/derivedclass diff --git a/packages/ckeditor5-dev-docs/tests/validators/link-validator/index.js b/packages/ckeditor5-dev-docs/tests/validators/link-validator/index.js index b9b112804..3ad28d4cf 100644 --- a/packages/ckeditor5-dev-docs/tests/validators/link-validator/index.js +++ b/packages/ckeditor5-dev-docs/tests/validators/link-validator/index.js @@ -3,46 +3,43 @@ * For licensing, see LICENSE.md. */ -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); -const testUtils = require( '../../utils' ); +import { describe, it, expect, vi } from 'vitest'; +import { fileURLToPath } from 'url'; +import path from 'path'; +import testUtils from '../../_utils.js'; +import build from '../../../lib/build.js'; -describe( 'dev-docs/validators/link-validator', function() { - this.timeout( 10 * 1000 ); - - const FIXTURES_PATH = testUtils.normalizePath( __dirname, 'fixtures' ); - const SOURCE_FILES = testUtils.normalizePath( FIXTURES_PATH, '*.ts' ); - const DERIVED_FILE = testUtils.normalizePath( FIXTURES_PATH, 'inheritance', 'derivedclass.ts' ); - const TSCONFIG_PATH = testUtils.normalizePath( FIXTURES_PATH, 'tsconfig.json' ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); - let build, logStub, warnStub, onErrorCallback; +const stubs = vi.hoisted( () => { + return { + onErrorCallback: vi.fn() + }; +} ); - beforeEach( async () => { - const validators = proxyquire( '../../../lib/validators', { - './link-validator': project => { - return require( '../../../lib/validators/link-validator' )( project, onErrorCallback ); - }, - './module-validator': sinon.spy() - } ); +vi.stubGlobal( 'console', { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn() +} ); - build = proxyquire( '../../../lib/buildtypedoc', { - './validators': validators - } ); +vi.mock( '../../../lib/validators/link-validator', async () => { + const { default: validator } = await vi.importActual( '../../../lib/validators/link-validator' ); - logStub = sinon.stub( console, 'log' ); - warnStub = sinon.stub( console, 'warn' ); - onErrorCallback = sinon.stub(); - } ); + return { + default: project => validator( project, ( ...args ) => stubs.onErrorCallback( ...args ) ) + }; +} ); - afterEach( () => { - logStub.restore(); - warnStub.restore(); - } ); +describe( 'dev-docs/validators/link-validator', function() { + const FIXTURES_PATH = testUtils.normalizePath( __dirname, 'fixtures' ); + const SOURCE_FILES = testUtils.normalizePath( FIXTURES_PATH, '*.ts' ); + const DERIVED_FILE = testUtils.normalizePath( FIXTURES_PATH, 'inheritance', 'derivedclass.ts' ); + const TSCONFIG_PATH = testUtils.normalizePath( FIXTURES_PATH, 'tsconfig.json' ); it( 'should warn if link is not valid', async () => { await build( { - type: 'typedoc', cwd: FIXTURES_PATH, tsconfig: TSCONFIG_PATH, sourceFiles: [ SOURCE_FILES ], @@ -112,25 +109,35 @@ describe( 'dev-docs/validators/link-validator', function() { } ]; - expect( onErrorCallback.callCount ).to.equal( expectedErrors.length ); + expect( stubs.onErrorCallback ).toHaveBeenCalledTimes( expectedErrors.length ); + + for ( const call of stubs.onErrorCallback.mock.calls ) { + expect( call ).toSatisfy( call => { + const [ message, reflection ] = call; + + return expectedErrors.some( error => { + if ( message !== `Incorrect link: "${ error.identifier }"` ) { + return false; + } + + if ( testUtils.getSource( reflection ) !== error.source ) { + return false; + } - for ( const error of expectedErrors ) { - expect( onErrorCallback ).to.be.calledWith( - `Incorrect link: "${ error.identifier }"`, - sinon.match( reflection => error.source === testUtils.getSource( reflection ) ) - ); + return true; + } ); + } ); } } ); it( 'should not call error callback for derived class when there are errors in inherited class', async () => { await build( { - type: 'typedoc', cwd: FIXTURES_PATH, tsconfig: TSCONFIG_PATH, sourceFiles: [ DERIVED_FILE ], strict: false } ); - expect( onErrorCallback.callCount ).to.equal( 0 ); + expect( stubs.onErrorCallback ).toHaveBeenCalledTimes( 0 ); } ); } ); diff --git a/packages/ckeditor5-dev-docs/tests/validators/module-validator/index.js b/packages/ckeditor5-dev-docs/tests/validators/module-validator/index.js index 28d864a6a..b194c2eb8 100644 --- a/packages/ckeditor5-dev-docs/tests/validators/module-validator/index.js +++ b/packages/ckeditor5-dev-docs/tests/validators/module-validator/index.js @@ -3,45 +3,48 @@ * For licensing, see LICENSE.md. */ -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); -const testUtils = require( '../../utils' ); +import { describe, it, expect, vi } from 'vitest'; +import { fileURLToPath } from 'url'; +import path from 'path'; +import testUtils from '../../_utils.js'; +import build from '../../../lib/build.js'; -describe( 'dev-docs/validators/module-validator', function() { - this.timeout( 10 * 1000 ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); - const FIXTURES_PATH = testUtils.normalizePath( __dirname, 'fixtures' ); - const SOURCE_FILES = testUtils.normalizePath( FIXTURES_PATH, '**', '*.ts' ); - const TSCONFIG_PATH = testUtils.normalizePath( FIXTURES_PATH, 'tsconfig.json' ); +const stubs = vi.hoisted( () => { + return { + onErrorCallback: vi.fn() + }; +} ); - const onErrorCallback = sinon.stub(); +vi.stubGlobal( 'console', { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn() +} ); - before( async () => { - const validators = proxyquire( '../../../lib/validators', { - './module-validator': project => { - return require( '../../../lib/validators/module-validator' )( project, onErrorCallback ); - } - } ); +vi.mock( '../../../lib/validators/module-validator', async () => { + const { default: validator } = await vi.importActual( '../../../lib/validators/module-validator' ); - const build = proxyquire( '../../../lib/buildtypedoc', { - './validators': validators - } ); + return { + default: project => validator( project, ( ...args ) => stubs.onErrorCallback( ...args ) ) + }; +} ); - const logStub = sinon.stub( console, 'log' ); +describe( 'dev-docs/validators/module-validator', function() { + const FIXTURES_PATH = testUtils.normalizePath( __dirname, 'fixtures' ); + const SOURCE_FILES = testUtils.normalizePath( FIXTURES_PATH, '**', '*.ts' ); + const TSCONFIG_PATH = testUtils.normalizePath( FIXTURES_PATH, 'tsconfig.json' ); + it( 'should warn if module name is not valid', async () => { await build( { - type: 'typedoc', cwd: FIXTURES_PATH, tsconfig: TSCONFIG_PATH, sourceFiles: [ SOURCE_FILES ], strict: false } ); - logStub.restore(); - } ); - - it( 'should warn if module name is not valid', () => { const expectedErrors = [ { source: 'ckeditor5-example/src/modulerootinvalid1.ts:10', @@ -69,13 +72,24 @@ describe( 'dev-docs/validators/module-validator', function() { } ]; - expect( onErrorCallback.callCount ).to.equal( expectedErrors.length ); + expect( stubs.onErrorCallback ).toHaveBeenCalledTimes( expectedErrors.length ); + + for ( const call of stubs.onErrorCallback.mock.calls ) { + expect( call ).toSatisfy( call => { + const [ message, reflection ] = call; + + return expectedErrors.some( error => { + if ( message !== `Invalid module name: "${ error.name }"` ) { + return false; + } + + if ( testUtils.getSource( reflection ) !== error.source ) { + return false; + } - for ( const error of expectedErrors ) { - expect( onErrorCallback ).to.be.calledWith( - `Invalid module name: "${ error.name }"`, - sinon.match( reflection => error.source === testUtils.getSource( reflection ) ) - ); + return true; + } ); + } ); } } ); } ); diff --git a/packages/ckeditor5-dev-docs/tests/validators/overloads-validator/index.js b/packages/ckeditor5-dev-docs/tests/validators/overloads-validator/index.js index b327bc28f..0f11bfc67 100644 --- a/packages/ckeditor5-dev-docs/tests/validators/overloads-validator/index.js +++ b/packages/ckeditor5-dev-docs/tests/validators/overloads-validator/index.js @@ -3,36 +3,42 @@ * For licensing, see LICENSE.md. */ -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); -const testUtils = require( '../../utils' ); +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { fileURLToPath } from 'url'; +import path from 'path'; +import testUtils from '../../_utils.js'; +import build from '../../../lib/build.js'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); + +const stubs = vi.hoisted( () => { + return { + onErrorCallback: vi.fn() + }; +} ); -describe( 'dev-docs/validators/overloads-validator', function() { - this.timeout( 10 * 1000 ); +vi.stubGlobal( 'console', { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn() +} ); +vi.mock( '../../../lib/validators/overloads-validator', async () => { + const { default: validator } = await vi.importActual( '../../../lib/validators/overloads-validator' ); + + return { + default: project => validator( project, ( ...args ) => stubs.onErrorCallback( ...args ) ) + }; +} ); + +describe( 'dev-docs/validators/overloads-validator', function() { const FIXTURES_PATH = testUtils.normalizePath( __dirname, 'fixtures' ); const SOURCE_FILES = testUtils.normalizePath( FIXTURES_PATH, '**', '*.ts' ); const TSCONFIG_PATH = testUtils.normalizePath( FIXTURES_PATH, 'tsconfig.json' ); - const onErrorCallback = sinon.stub(); - - before( async () => { - const validators = proxyquire( '../../../lib/validators', { - './overloads-validator': project => { - return require( '../../../lib/validators/overloads-validator' )( project, onErrorCallback ); - }, - './module-validator': sinon.spy() - } ); - - const build = proxyquire( '../../../lib/buildtypedoc', { - './validators': validators - } ); - - const logStub = sinon.stub( console, 'log' ); - + beforeEach( async () => { await build( { - type: 'typedoc', cwd: FIXTURES_PATH, tsconfig: TSCONFIG_PATH, sourceFiles: [ SOURCE_FILES ], @@ -41,8 +47,6 @@ describe( 'dev-docs/validators/overloads-validator', function() { enableOverloadValidator: true } } ); - - logStub.restore(); } ); it( 'should warn if overloaded signature does not have "@label" tag', () => { @@ -53,17 +57,21 @@ describe( 'dev-docs/validators/overloads-validator', function() { { source: 'overloadsinvalid.ts:24' } ]; - const errorCalls = onErrorCallback.getCalls().filter( call => { - return call.args[ 0 ] === 'Overloaded signature misses the @label tag'; + const errorCalls = stubs.onErrorCallback.mock.calls.filter( ( [ message ] ) => { + return message === 'Overloaded signature misses the @label tag'; } ); expect( errorCalls.length ).to.equal( expectedErrors.length ); - expectedErrors.forEach( ( { source }, index ) => { - const currentValue = testUtils.getSource( errorCalls[ index ].args[ 1 ] ); + for ( const call of errorCalls ) { + expect( call ).toSatisfy( call => { + const [ , reflection ] = call; - expect( currentValue ).to.equal( source ); - } ); + return expectedErrors.some( error => { + return testUtils.getSource( reflection ) === error.source; + } ); + } ); + } } ); it( 'should warn if overloaded signatures use the same identifier', () => { @@ -71,18 +79,28 @@ describe( 'dev-docs/validators/overloads-validator', function() { { source: 'overloadsinvalid.ts:51', error: 'Duplicated name: "NOT_SO_UNIQUE" in the @label tag' } ]; - const errorCalls = onErrorCallback.getCalls().filter( call => { - return call.args[ 0 ].startsWith( 'Duplicated name' ); + const errorCalls = stubs.onErrorCallback.mock.calls.filter( ( [ message ] ) => { + return message.startsWith( 'Duplicated name' ); } ); expect( errorCalls.length ).to.equal( expectedErrors.length ); - expectedErrors.forEach( ( { source, error }, index ) => { - const [ message, reflection ] = errorCalls[ index ].args; - const currentValue = testUtils.getSource( reflection ); + for ( const call of errorCalls ) { + expect( call ).toSatisfy( call => { + const [ message, reflection ] = call; - expect( message ).to.equal( error ); - expect( currentValue ).to.equal( source ); - } ); + return expectedErrors.some( ( { source, error } ) => { + if ( message !== error ) { + return false; + } + + if ( testUtils.getSource( reflection ) !== source ) { + return false; + } + + return true; + } ); + } ); + } } ); } ); diff --git a/packages/ckeditor5-dev-docs/tests/validators/see-validator/index.js b/packages/ckeditor5-dev-docs/tests/validators/see-validator/index.js index e6ae545b3..78ab261ad 100644 --- a/packages/ckeditor5-dev-docs/tests/validators/see-validator/index.js +++ b/packages/ckeditor5-dev-docs/tests/validators/see-validator/index.js @@ -3,46 +3,48 @@ * For licensing, see LICENSE.md. */ -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); -const testUtils = require( '../../utils' ); +import { describe, it, expect, vi } from 'vitest'; +import { fileURLToPath } from 'url'; +import path from 'path'; +import testUtils from '../../_utils.js'; +import build from '../../../lib/build.js'; -describe( 'dev-docs/validators/see-validator', function() { - this.timeout( 10 * 1000 ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); - const FIXTURES_PATH = testUtils.normalizePath( __dirname, 'fixtures' ); - const SOURCE_FILES = testUtils.normalizePath( FIXTURES_PATH, '**', '*.ts' ); - const TSCONFIG_PATH = testUtils.normalizePath( FIXTURES_PATH, 'tsconfig.json' ); +const stubs = vi.hoisted( () => { + return { + onErrorCallback: vi.fn() + }; +} ); - const onErrorCallback = sinon.stub(); +vi.stubGlobal( 'console', { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn() +} ); - before( async () => { - const validators = proxyquire( '../../../lib/validators', { - './see-validator': project => { - return require( '../../../lib/validators/see-validator' )( project, onErrorCallback ); - }, - './module-validator': sinon.spy() - } ); +vi.mock( '../../../lib/validators/see-validator', async () => { + const { default: validator } = await vi.importActual( '../../../lib/validators/see-validator' ); - const build = proxyquire( '../../../lib/buildtypedoc', { - './validators': validators - } ); + return { + default: project => validator( project, ( ...args ) => stubs.onErrorCallback( ...args ) ) + }; +} ); - const logStub = sinon.stub( console, 'log' ); +describe( 'dev-docs/validators/see-validator', function() { + const FIXTURES_PATH = testUtils.normalizePath( __dirname, 'fixtures' ); + const SOURCE_FILES = testUtils.normalizePath( FIXTURES_PATH, '**', '*.ts' ); + const TSCONFIG_PATH = testUtils.normalizePath( FIXTURES_PATH, 'tsconfig.json' ); + it( 'should warn if link is not valid', async () => { await build( { - type: 'typedoc', cwd: FIXTURES_PATH, tsconfig: TSCONFIG_PATH, sourceFiles: [ SOURCE_FILES ], strict: false } ); - logStub.restore(); - } ); - - it( 'should warn if link is not valid', () => { const expectedErrors = [ { identifier: '.property', @@ -98,13 +100,24 @@ describe( 'dev-docs/validators/see-validator', function() { } ]; - expect( onErrorCallback.callCount ).to.equal( expectedErrors.length ); + expect( stubs.onErrorCallback ).toHaveBeenCalledTimes( expectedErrors.length ); + + for ( const call of stubs.onErrorCallback.mock.calls ) { + expect( call ).toSatisfy( call => { + const [ message, reflection ] = call; + + return expectedErrors.some( error => { + if ( message !== `Incorrect link: "${ error.identifier }"` ) { + return false; + } + + if ( testUtils.getSource( reflection ) !== error.source ) { + return false; + } - for ( const error of expectedErrors ) { - expect( onErrorCallback ).to.be.calledWith( - `Incorrect link: "${ error.identifier }"`, - sinon.match( reflection => error.source === testUtils.getSource( reflection ) ) - ); + return true; + } ); + } ); } } ); } ); diff --git a/packages/ckeditor5-dev-docs/vitest.config.js b/packages/ckeditor5-dev-docs/vitest.config.js new file mode 100644 index 000000000..48441e92f --- /dev/null +++ b/packages/ckeditor5-dev-docs/vitest.config.js @@ -0,0 +1,26 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig( { + test: { + testTimeout: 10000, + restoreMocks: true, + include: [ + 'tests/**/*.js' + ], + exclude: [ + 'tests/_utils.js' + ], + coverage: { + provider: 'v8', + include: [ + 'lib/**' + ], + reporter: [ 'text', 'json', 'html', 'lcov' ] + } + } +} ); diff --git a/packages/ckeditor5-dev-release-tools/lib/index.js b/packages/ckeditor5-dev-release-tools/lib/index.js index 5fe738e25..43fc56853 100644 --- a/packages/ckeditor5-dev-release-tools/lib/index.js +++ b/packages/ckeditor5-dev-release-tools/lib/index.js @@ -3,20 +3,18 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const generateChangelogForSinglePackage = require( './tasks/generatechangelogforsinglepackage' ); -const generateChangelogForMonoRepository = require( './tasks/generatechangelogformonorepository' ); -const updateDependencies = require( './tasks/updatedependencies' ); -const commitAndTag = require( './tasks/commitandtag' ); -const createGithubRelease = require( './tasks/creategithubrelease' ); -const reassignNpmTags = require( './tasks/reassignnpmtags' ); -const prepareRepository = require( './tasks/preparerepository' ); -const push = require( './tasks/push' ); -const publishPackages = require( './tasks/publishpackages' ); -const updateVersions = require( './tasks/updateversions' ); -const cleanUpPackages = require( './tasks/cleanuppackages' ); -const { +export { default as generateChangelogForSinglePackage } from './tasks/generatechangelogforsinglepackage.js'; +export { default as generateChangelogForMonoRepository } from './tasks/generatechangelogformonorepository.js'; +export { default as updateDependencies } from './tasks/updatedependencies.js'; +export { default as commitAndTag } from './tasks/commitandtag.js'; +export { default as createGithubRelease } from './tasks/creategithubrelease.js'; +export { default as reassignNpmTags } from './tasks/reassignnpmtags.js'; +export { default as prepareRepository } from './tasks/preparerepository.js'; +export { default as push } from './tasks/push.js'; +export { default as publishPackages } from './tasks/publishpackages.js'; +export { default as updateVersions } from './tasks/updateversions.js'; +export { default as cleanUpPackages } from './tasks/cleanuppackages.js'; +export { getLastFromChangelog, getLastPreRelease, getNextPreRelease, @@ -24,41 +22,14 @@ const { getNextNightly, getCurrent, getLastTagFromGit -} = require( './utils/versions' ); -const { getChangesForVersion, getChangelog, saveChangelog } = require( './utils/changelog' ); -const executeInParallel = require( './utils/executeinparallel' ); -const validateRepositoryToRelease = require( './utils/validaterepositorytorelease' ); -const checkVersionAvailability = require( './utils/checkversionavailability' ); -const verifyPackagesPublishedCorrectly = require( './tasks/verifypackagespublishedcorrectly' ); -const getNpmTagFromVersion = require( './utils/getnpmtagfromversion' ); -const isVersionPublishableForTag = require( './utils/isversionpublishablefortag' ); - -module.exports = { - generateChangelogForSinglePackage, - generateChangelogForMonoRepository, - updateDependencies, - updateVersions, - prepareRepository, - commitAndTag, - createGithubRelease, - push, - cleanUpPackages, - publishPackages, - reassignNpmTags, - executeInParallel, - getLastFromChangelog, - getLastPreRelease, - getNextPreRelease, - getLastNightly, - getNextNightly, - getCurrent, - getLastTagFromGit, - getNpmTagFromVersion, - getChangesForVersion, - getChangelog, - saveChangelog, - validateRepositoryToRelease, - verifyPackagesPublishedCorrectly, - checkVersionAvailability, - isVersionPublishableForTag -}; +} from './utils/versions.js'; +export { default as getChangesForVersion } from './utils/getchangesforversion.js'; +export { default as getChangelog } from './utils/getchangelog.js'; +export { default as saveChangelog } from './utils/savechangelog.js'; +export { default as executeInParallel } from './utils/executeinparallel.js'; +export { default as validateRepositoryToRelease } from './utils/validaterepositorytorelease.js'; +export { default as checkVersionAvailability } from './utils/checkversionavailability.js'; +export { default as verifyPackagesPublishedCorrectly } from './tasks/verifypackagespublishedcorrectly.js'; +export { default as getNpmTagFromVersion } from './utils/getnpmtagfromversion.js'; +export { default as isVersionPublishableForTag } from './utils/isversionpublishablefortag.js'; +export { default as provideToken } from './utils/providetoken.js'; diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/cleanuppackages.js b/packages/ckeditor5-dev-release-tools/lib/tasks/cleanuppackages.js index 7e6e7c420..7b33fe1d9 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/cleanuppackages.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/cleanuppackages.js @@ -3,11 +3,9 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs-extra' ); -const upath = require( 'upath' ); -const { glob } = require( 'glob' ); +import fs from 'fs-extra'; +import upath from 'upath'; +import { glob } from 'glob'; /** * The purpose of the script is to clean all packages prepared for the release. The cleaning consists of two stages: @@ -21,15 +19,15 @@ const { glob } = require( 'glob' ); * - file pointed by the `types` field from `package.json` * - Removes unnecessary fields from the `package.json` file. * - * @param {Object} options - * @param {String} options.packagesDirectory Relative path to a location of packages to be cleaned up. - * @param {Array.|PackageJsonFieldsToRemoveCallback} [options.packageJsonFieldsToRemove] Fields to remove from `package.json`. + * @param {object} options + * @param {string} options.packagesDirectory Relative path to a location of packages to be cleaned up. + * @param {Array.|PackageJsonFieldsToRemoveCallback} [options.packageJsonFieldsToRemove] Fields to remove from `package.json`. * If not set, a predefined list is used. If the callback is used, the first argument is the list with defaults. - * @param {Boolean} [options.preservePostInstallHook] Whether to preserve the postinstall hook in `package.json`. - * @param {String} [options.cwd] Current working directory from which all paths will be resolved. + * @param {boolean} [options.preservePostInstallHook] Whether to preserve the postinstall hook in `package.json`. + * @param {string} [options.cwd] Current working directory from which all paths will be resolved. * @returns {Promise} */ -module.exports = async function cleanUpPackages( options ) { +export default async function cleanUpPackages( options ) { const { packagesDirectory, packageJsonFieldsToRemove, preservePostInstallHook, cwd } = parseOptions( options ); const packageJsonPaths = await glob( '*/package.json', { @@ -47,17 +45,17 @@ module.exports = async function cleanUpPackages( options ) { await fs.writeJson( packageJsonPath, packageJson, { spaces: 2 } ); } -}; +} /** * Prepares the configuration options for the script. * - * @param {Object} options - * @param {String} options.packagesDirectory - * @param {Array.|PackageJsonFieldsToRemoveCallback} [options.packageJsonFieldsToRemove=DefaultFieldsToRemove] - * @param {Boolean} [options.preservePostInstallHook] - * @param {String} [options.cwd=process.cwd()] - * @returns {Object} + * @param {object} options + * @param {string} options.packagesDirectory + * @param {Array.|PackageJsonFieldsToRemoveCallback} [options.packageJsonFieldsToRemove=DefaultFieldsToRemove] + * @param {boolean} [options.preservePostInstallHook] + * @param {string} [options.cwd=process.cwd()] + * @returns {object} */ function parseOptions( options ) { const defaultPackageJsonFieldsToRemove = [ 'devDependencies', 'depcheckIgnore', 'scripts', 'private' ]; @@ -81,8 +79,8 @@ function parseOptions( options ) { /** * Removes unnecessary files and directories from the package directory. * - * @param {Object} packageJson - * @param {String} packagePath + * @param {object} packageJson + * @param {string} packagePath * @returns {Promise} */ async function cleanUpPackageDirectory( packageJson, packagePath ) { @@ -131,8 +129,8 @@ async function cleanUpPackageDirectory( packageJson, packagePath ) { /** * Creates an array of patterns to ignore for the `glob` calls. * - * @param {Object} packageJson - * @returns {Array.} + * @param {object} packageJson + * @returns {Array.} */ function getIgnoredFilePatterns( packageJson ) { // The patterns supported by `package.json` in the `files` field do not correspond 1:1 to the patterns expected by the `glob`. @@ -157,9 +155,9 @@ function getIgnoredFilePatterns( packageJson ) { /** * Removes unnecessary fields from the `package.json`. * - * @param {Object} packageJson - * @param {Array.} packageJsonFieldsToRemove - * @param {Boolean} preservePostInstallHook + * @param {object} packageJson + * @param {Array.} packageJsonFieldsToRemove + * @param {boolean} preservePostInstallHook */ function cleanUpPackageJson( packageJson, packageJsonFieldsToRemove, preservePostInstallHook ) { for ( const key of Object.keys( packageJson ) ) { @@ -178,9 +176,9 @@ function cleanUpPackageJson( packageJson, packageJsonFieldsToRemove, preservePos /** * Sort function that defines the order of the paths. It sorts paths from the most nested ones first. * - * @param {String} firstPath - * @param {String} secondPath - * @returns {Number} + * @param {string} firstPath + * @param {string} secondPath + * @returns {number} */ function sortPathsFromDeepestFirst( firstPath, secondPath ) { const firstPathSegments = firstPath.split( '/' ).length; @@ -196,5 +194,5 @@ function sortPathsFromDeepestFirst( firstPath, secondPath ) { /** * @callback PackageJsonFieldsToRemoveCallback * @param {DefaultFieldsToRemove} defaults - * @returns {Array.} + * @returns {Array.} */ diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/commitandtag.js b/packages/ckeditor5-dev-release-tools/lib/tasks/commitandtag.js index 510e1a7be..dedde23a1 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/commitandtag.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/commitandtag.js @@ -3,23 +3,23 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import upath from 'upath'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import { glob } from 'glob'; +import shellEscape from 'shell-escape'; -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); -const { toUnix } = require( 'upath' ); -const { glob } = require( 'glob' ); -const shellEscape = require( 'shell-escape' ); +const { toUnix } = upath; /** * Creates a commit and a tag for specified version. * - * @param {Object} options - * @param {String} options.version The commit will contain this param in its message and the tag will have a `v` prefix. - * @param {Array.} options.files Array of glob patterns for files to be added to the release commit. - * @param {String} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. + * @param {object} options + * @param {string} options.version The commit will contain this param in its message and the tag will have a `v` prefix. + * @param {Array.} options.files Array of glob patterns for files to be added to the release commit. + * @param {string} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. * @returns {Promise} */ -module.exports = async function commitAndTag( { version, files, cwd = process.cwd() } ) { +export default async function commitAndTag( { version, files, cwd = process.cwd() } ) { const normalizedCwd = toUnix( cwd ); const filePathsToAdd = await glob( files, { cwd: normalizedCwd, absolute: true, nodir: true } ); @@ -38,7 +38,11 @@ module.exports = async function commitAndTag( { version, files, cwd = process.cw await tools.shExec( `git add ${ shellEscape( [ filePath ] ) }`, shExecOptions ); } - const escapedVersion = shellEscape( [ version ] ); - await tools.shExec( `git commit --message "Release: v${ escapedVersion }." --no-verify`, shExecOptions ); - await tools.shExec( `git tag v${ escapedVersion }`, shExecOptions ); -}; + const escapedVersion = { + commit: shellEscape( [ `Release: v${ version }.` ] ), + tag: shellEscape( [ `v${ version }` ] ) + }; + + await tools.shExec( `git commit --message ${ escapedVersion.commit } --no-verify`, shExecOptions ); + await tools.shExec( `git tag ${ escapedVersion.tag }`, shExecOptions ); +} diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/creategithubrelease.js b/packages/ckeditor5-dev-release-tools/lib/tasks/creategithubrelease.js index d7f49876d..5da69a476 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/creategithubrelease.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/creategithubrelease.js @@ -3,23 +3,23 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { Octokit } from '@octokit/rest'; +import semver from 'semver'; +import * as transformCommitUtils from '../utils/transformcommitutils.js'; -const { Octokit } = require( '@octokit/rest' ); -const semver = require( 'semver' ); -const { getRepositoryUrl } = require( '../utils/transformcommitutils' ); +const { getRepositoryUrl } = transformCommitUtils; /** * Create a GitHub release. * - * @param {Object} options - * @param {String} options.token Token used to authenticate with GitHub. - * @param {String} options.version Name of tag connected with the release. - * @param {String} options.description Description of the release. - * @param {String} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. - * @returns {Promise.} + * @param {object} options + * @param {string} options.token Token used to authenticate with GitHub. + * @param {string} options.version Name of tag connected with the release. + * @param {string} options.description Description of the release. + * @param {string} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. + * @returns {Promise.} */ -module.exports = async function createGithubRelease( options ) { +export default async function createGithubRelease( options ) { const { token, version, @@ -46,13 +46,13 @@ module.exports = async function createGithubRelease( options ) { } return `https://github.com/${ repositoryOwner }/${ repositoryName }/releases/tag/v${ version }`; -}; +} /** * Returns an npm tag based on the specified release version. * - * @param {String} version - * @returns {String} + * @param {string} version + * @returns {string} */ function getVersionTag( version ) { const [ versionTag ] = semver.prerelease( version ) || [ 'latest' ]; @@ -64,9 +64,9 @@ function getVersionTag( version ) { * Resolves a promise containing a flag if the GitHub contains the release page for given version. * * @param {Octokit} github - * @param {String} repositoryOwner - * @param {String} repositoryName - * @param {String} version + * @param {string} repositoryOwner + * @param {string} repositoryName + * @param {string} version * @returns {Promise.} */ async function shouldCreateRelease( github, repositoryOwner, repositoryName, version ) { diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/generatechangelogformonorepository.js b/packages/ckeditor5-dev-release-tools/lib/tasks/generatechangelogformonorepository.js index 85e798fda..ff6e6643e 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/generatechangelogformonorepository.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/generatechangelogformonorepository.js @@ -3,26 +3,28 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const { tools, logger } = require( '@ckeditor/ckeditor5-dev-utils' ); -const compareFunc = require( 'compare-func' ); -const chalk = require( 'chalk' ); -const semver = require( 'semver' ); -const changelogUtils = require( '../utils/changelog' ); -const cli = require( '../utils/cli' ); -const displayCommits = require( '../utils/displaycommits' ); -const displaySkippedPackages = require( '../utils/displayskippedpackages' ); -const generateChangelog = require( '../utils/generatechangelog' ); -const getPackageJson = require( '../utils/getpackagejson' ); -const getPackagesPaths = require( '../utils/getpackagespaths' ); -const getCommits = require( '../utils/getcommits' ); -const getNewVersionType = require( '../utils/getnewversiontype' ); -const getWriterOptions = require( '../utils/getwriteroptions' ); -const { getRepositoryUrl } = require( '../utils/transformcommitutils' ); -const transformCommitFactory = require( '../utils/transformcommitfactory' ); +import fs from 'fs'; +import path from 'path'; +import { tools, logger } from '@ckeditor/ckeditor5-dev-utils'; +import compareFunc from 'compare-func'; +import chalk from 'chalk'; +import semver from 'semver'; +import displayCommits from '../utils/displaycommits.js'; +import displaySkippedPackages from '../utils/displayskippedpackages.js'; +import generateChangelog from '../utils/generatechangelog.js'; +import getPackageJson from '../utils/getpackagejson.js'; +import getPackagesPaths from '../utils/getpackagespaths.js'; +import getCommits from '../utils/getcommits.js'; +import getNewVersionType from '../utils/getnewversiontype.js'; +import getWriterOptions from '../utils/getwriteroptions.js'; +import getFormattedDate from '../utils/getformatteddate.js'; +import getChangelog from '../utils/getchangelog.js'; +import saveChangelog from '../utils/savechangelog.js'; +import truncateChangelog from '../utils/truncatechangelog.js'; +import transformCommitFactory from '../utils/transformcommitfactory.js'; +import { getRepositoryUrl } from '../utils/transformcommitutils.js'; +import provideNewVersionForMonoRepository from '../utils/providenewversionformonorepository.js'; +import { CHANGELOG_FILE, CHANGELOG_HEADER, CLI_INDENT_SIZE } from '../utils/constants.js'; const VERSIONING_POLICY_URL = 'https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html'; const noteInfo = `[ℹ️](${ VERSIONING_POLICY_URL }#major-and-minor-breaking-changes)`; @@ -33,37 +35,39 @@ const noteInfo = `[ℹ️](${ VERSIONING_POLICY_URL }#major-and-minor-breaking-c * * The typed version will be the same for all packages. See: https://github.com/ckeditor/ckeditor5/issues/7323. * - * @param {Object} options + * @param {object} options * - * @param {String} options.cwd Current working directory (packages) from which all paths will be resolved. + * @param {string} options.cwd Current working directory (packages) from which all paths will be resolved. * - * @param {String} options.packages Where to look for packages. + * @param {string} options.packages Where to look for packages. * - * @param {Function} options.transformScope A function that returns a URL to a package from a scope of a commit. + * @param {function} options.transformScope A function that returns a URL to a package from a scope of a commit. * - * @param {String} [options.scope] Package names have to match to specified glob pattern in order to be processed. + * @param {string} [options.scope] Package names have to match to specified glob pattern in order to be processed. * - * @param {Array.} [options.skipPackages=[]] Name of packages which won't be touched. + * @param {Array.} [options.skipPackages=[]] Name of packages which won't be touched. * - * @param {Boolean} [options.skipLinks=false] If set on true, links to release or commits will be omitted. + * @param {boolean} [options.skipLinks=false] If set on true, links to release or commits will be omitted. * - * @param {String} [options.from] A commit or tag name that will be the first param of the range of commits to collect. + * @param {string} [options.from] A commit or tag name that will be the first param of the range of commits to collect. * - * @param {String} [options.releaseBranch='master'] A name of the branch that should be used for releasing packages. + * @param {string} [options.releaseBranch='master'] A name of the branch that should be used for releasing packages. + * + * @param {string} [options.mainBranch='master'] A name of the main branch in the repository. * * @param {Array.} [options.externalRepositories=[]] An array of object with additional repositories * that the function takes into consideration while gathering commits. It assumes that those directories are also mono repositories. * - * @param {Boolean} [options.skipFileSave=false] Whether to resolve the changes instead of saving it to a file. + * @param {boolean} [options.skipFileSave=false] Whether to resolve the changes instead of saving it to a file. * - * @param {String|null} [options.nextVersion=null] Next version to use. If not provided, a user needs to provide via CLI. + * @param {string|null} [options.nextVersion=null] Next version to use. If not provided, a user needs to provide via CLI. * * @param {FormatDateCallback} [options.formatDate] A callback allowing defining a custom format of the date inserted into the changelog. * If not specified, the default date matches the `YYYY-MM-DD` pattern. * - * @returns {Promise.} + * @returns {Promise.} */ -module.exports = async function generateChangelogForMonoRepository( options ) { +export default async function generateChangelogForMonoRepository( options ) { const log = logger(); const cwd = process.cwd(); const rootPkgJson = getPackageJson( options.cwd ); @@ -83,6 +87,7 @@ module.exports = async function generateChangelogForMonoRepository( options ) { cwd: options.cwd, from: options.from ? options.from : 'v' + rootPkgJson.version, releaseBranch: options.releaseBranch || 'master', + mainBranch: options.mainBranch || 'master', externalRepositories: options.externalRepositories || [] }; @@ -125,11 +130,11 @@ module.exports = async function generateChangelogForMonoRepository( options ) { } if ( !skipFileSave ) { - await saveChangelog(); + await saveChangelogToFile(); // Make a commit from the repository where we started. process.chdir( options.cwd ); - tools.shExec( `git add ${ changelogUtils.changelogFile }`, { verbosity: 'error' } ); + tools.shExec( `git add ${ CHANGELOG_FILE }`, { verbosity: 'error' } ); tools.shExec( 'git commit -m "Docs: Changelog. [skip ci]"', { verbosity: 'error' } ); logInfo( 'Committed.', { indentLevel: 1 } ); } @@ -156,11 +161,11 @@ module.exports = async function generateChangelogForMonoRepository( options ) { /** * Returns collections with packages found in the `options.cwd` directory and the external repositories. * - * @param {Object} options - * @param {String} options.cwd Current working directory (packages) from which all paths will be resolved. - * @param {String} options.packages Where to look for packages. - * @param {String} options.scope Package names have to match to specified glob pattern in order to be processed. - * @param {Array.} options.skipPackages Name of packages which won't be touched. + * @param {object} options + * @param {string} options.cwd Current working directory (packages) from which all paths will be resolved. + * @param {string} options.packages Where to look for packages. + * @param {string} options.scope Package names have to match to specified glob pattern in order to be processed. + * @param {Array.} options.skipPackages Name of packages which won't be touched. * @param {Array.} options.externalRepositories An array of object with additional repositories * that the function takes into consideration while gathering packages. * @returns {PathsCollection} @@ -205,10 +210,10 @@ module.exports = async function generateChangelogForMonoRepository( options ) { /** * Returns a promise that resolves an array of commits since the last tag specified as `options.from`. * - * @param {Object} options - * @param {String} options.cwd Current working directory (packages) from which all paths will be resolved. - * @param {String} options.from A commit or tag name that will be the first param of the range of commits to collect. - * @param {String} options.releaseBranch A name of the branch that should be used for releasing packages. + * @param {object} options + * @param {string} options.cwd Current working directory (packages) from which all paths will be resolved. + * @param {string} options.from A commit or tag name that will be the first param of the range of commits to collect. + * @param {string} options.releaseBranch A name of the branch that should be used for releasing packages. * @param {Array.} options.externalRepositories An array of object with additional repositories * that the function takes into consideration while gathering commits. * @returns {Promise.>} @@ -223,7 +228,8 @@ module.exports = async function generateChangelogForMonoRepository( options ) { const commitOptions = { from: options.from, - releaseBranch: options.releaseBranch + releaseBranch: options.releaseBranch, + mainBranch: options.mainBranch }; let promise = getCommits( transformCommit, commitOptions ) @@ -295,7 +301,7 @@ module.exports = async function generateChangelogForMonoRepository( options ) { bumpType = 'patch'; } - return cli.provideNewVersionForMonoRepository( highestVersion, packageHighestVersion, bumpType, { indentLevel: 1 } ) + return provideNewVersionForMonoRepository( highestVersion, packageHighestVersion, bumpType, { indentLevel: 1 } ) .then( version => { nextVersion = version; @@ -372,7 +378,7 @@ module.exports = async function generateChangelogForMonoRepository( options ) { * Finds commits that touched the package under `packagePath` directory. * * @param {Array.} commits - * @param {String} packagePath + * @param {string} packagePath * @returns {Array.} */ function filterCommitsByPath( commits, packagePath ) { @@ -394,7 +400,7 @@ module.exports = async function generateChangelogForMonoRepository( options ) { /** * Generates a list of changes based on the commits in the main repository. * - * @returns {Promise.} + * @returns {Promise.} */ function generateChangelogFromCommits() { logProcess( 'Generating the changelog...' ); @@ -410,12 +416,12 @@ module.exports = async function generateChangelogForMonoRepository( options ) { isPatch: semver.diff( version, rootPkgJson.version ) === 'patch', skipCommitsLink: Boolean( options.skipLinks ), skipCompareLink: Boolean( options.skipLinks ), - date: options.formatDate ? options.formatDate( new Date() ) : changelogUtils.getFormattedDate() + date: options.formatDate ? options.formatDate( new Date() ) : getFormattedDate() }; - const writerOptions = getWriterOptions( { + const writerOptions = getWriterOptions( commit => { // We do not allow modifying the commit hash value by the generator itself. - hash: hash => hash + return commit; } ); writerOptions.commitsSort = sortFunctionFactory( 'rawScope' ); @@ -464,7 +470,7 @@ module.exports = async function generateChangelogForMonoRepository( options ) { const dependenciesSummary = generateSummaryOfChangesInPackages(); return [ - changelogUtils.changelogHeader, + CHANGELOG_HEADER, changesFromCommits.trim(), '\n\n', dependenciesSummary @@ -474,26 +480,24 @@ module.exports = async function generateChangelogForMonoRepository( options ) { /** * Combines the generated changes based on commits and summary of version changes in packages. * Appends those changes at the beginning of the changelog file. - * - * @param {String} changesFromCommits Generated entries based on commits. */ - async function saveChangelog() { + async function saveChangelogToFile() { logProcess( 'Saving changelog...' ); - if ( !fs.existsSync( changelogUtils.changelogFile ) ) { + if ( !fs.existsSync( CHANGELOG_FILE ) ) { logInfo( 'Changelog file does not exist. Creating...', { isWarning: true, indentLevel: 1 } ); - changelogUtils.saveChangelog( changelogUtils.changelogHeader ); + saveChangelog( CHANGELOG_FILE ); } logInfo( 'Preparing a summary of version changes in packages.', { indentLevel: 1 } ); - let currentChangelog = changelogUtils.getChangelog(); + let currentChangelog = getChangelog(); const nextVersionChangelog = await getChangelogForNextVersion(); // Remove header from current changelog. - currentChangelog = currentChangelog.replace( changelogUtils.changelogHeader, '' ).trim(); + currentChangelog = currentChangelog.replace( CHANGELOG_HEADER, '' ).trim(); // Concat header, new entries and old changelog to single string. let newChangelog = nextVersionChangelog + '\n\n\n' + currentChangelog; @@ -501,10 +505,10 @@ module.exports = async function generateChangelogForMonoRepository( options ) { newChangelog = newChangelog.trim() + '\n'; // Save the changelog. - changelogUtils.saveChangelog( newChangelog ); + saveChangelog( newChangelog ); // Truncate the changelog to keep the latest five release entries. - changelogUtils.truncateChangelog( 5 ); + truncateChangelog( 5 ); logInfo( 'Saved.', { indentLevel: 1 } ); } @@ -512,7 +516,7 @@ module.exports = async function generateChangelogForMonoRepository( options ) { /** * Prepares a summary that describes what has changed in all dependencies. * - * @returns {String} + * @returns {string} */ function generateSummaryOfChangesInPackages() { const dependencies = new Map(); @@ -593,8 +597,8 @@ module.exports = async function generateChangelogForMonoRepository( options ) { } /** - * @param {Map.} dependencies - * @returns {Map.} + * @param {Map.} dependencies + * @returns {Map.} */ function getNewPackages( dependencies ) { const packages = new Map(); @@ -612,9 +616,9 @@ module.exports = async function generateChangelogForMonoRepository( options ) { /** * Returns packages where scope of changes described in the commits' notes match to packages' names. * - * @param {Map.} dependencies - * @param {String} noteTitle - * @returns {Map.} + * @param {Map.} dependencies + * @param {string} noteTitle + * @returns {Map.} */ function getPackagesMatchedToScopesFromNotes( dependencies, noteTitle ) { const packages = new Map(); @@ -645,8 +649,8 @@ module.exports = async function generateChangelogForMonoRepository( options ) { /** * Returns packages that contain new features. * - * @param {Map.} dependencies - * @returns {Map.} + * @param {Map.} dependencies + * @returns {Map.} */ function getPackagesWithNewFeatures( dependencies ) { const packages = new Map(); @@ -668,10 +672,10 @@ module.exports = async function generateChangelogForMonoRepository( options ) { /** * Returns a formatted entry (string) for the changelog. * - * @param {String} packageName - * @param {String} nextVersion - * @param {String} currentVersion - * @returns {String} + * @param {string} packageName + * @param {string} nextVersion + * @param {string} currentVersion + * @returns {string} */ function formatChangelogEntry( packageName, nextVersion, currentVersion = null ) { const npmUrl = `https://www.npmjs.com/package/${ packageName }/v/${ nextVersion }`; @@ -686,7 +690,7 @@ module.exports = async function generateChangelogForMonoRepository( options ) { /** * Returns a function that is being used when sorting commits. * - * @param {String} scopeField A name of the field that saves the commit's scope. + * @param {string} scopeField A name of the field that saves the commit's scope. * @returns {Function} */ function sortFunctionFactory( scopeField ) { @@ -710,46 +714,46 @@ module.exports = async function generateChangelogForMonoRepository( options ) { } /** - * @param {String} message - * @param {Object} [options={}] - * @param {Number} [options.indentLevel=0] - * @param {Boolean} [options.startWithNewLine=false] Whether to append a new line before the message. - * @param {Boolean} [options.isWarning=false] Whether to use `warning` method instead of `log`. + * @param {string} message + * @param {object} [options={}] + * @param {number} [options.indentLevel=0] + * @param {boolean} [options.startWithNewLine=false] Whether to append a new line before the message. + * @param {boolean} [options.isWarning=false] Whether to use `warning` method instead of `log`. */ function logInfo( message, options = {} ) { const indentLevel = options.indentLevel || 0; const startWithNewLine = options.startWithNewLine || false; const method = options.isWarning ? 'warning' : 'info'; - log[ method ]( `${ startWithNewLine ? '\n' : '' }${ ' '.repeat( indentLevel * cli.INDENT_SIZE ) }` + message ); + log[ method ]( `${ startWithNewLine ? '\n' : '' }${ ' '.repeat( indentLevel * CLI_INDENT_SIZE ) }` + message ); } -}; +} /** - * @typedef {Object} Version + * @typedef {object} Version * - * @param {Boolean} current The current version defined in the `package.json` file. + * @param {boolean} current The current version defined in the `package.json` file. * - * @param {Boolean} next The next version defined during generating the changelog file. + * @param {boolean} next The next version defined during generating the changelog file. */ /** - * @typedef {Object} ExternalRepository + * @typedef {object} ExternalRepository * - * @param {String} cwd An absolute path to the repository. + * @param {string} cwd An absolute path to the repository. * - * @param {String} packages Subdirectory in a given `cwd` that should searched for packages. E.g. `'packages'`. + * @param {string} packages Subdirectory in a given `cwd` that should searched for packages. E.g. `'packages'`. * - * @param {String} [scope] Glob pattern for package names to be processed. + * @param {string} [scope] Glob pattern for package names to be processed. * - * @param {Array.} [skipPackages] Name of packages which won't be touched. + * @param {Array.} [skipPackages] Name of packages which won't be touched. * - * @param {Boolean} [skipLinks] If set on `true`, a URL to commit (hash) will be omitted. + * @param {boolean} [skipLinks] If set on `true`, a URL to commit (hash) will be omitted. * - * @param {String} [from] A commit or tag name that will be the first param of the range of commits to collect. If not specified, + * @param {string} [from] A commit or tag name that will be the first param of the range of commits to collect. If not specified, * the option will inherit its value from the function's `options` object. * - * @param {String} [releaseBranch] A name of the branch that should be used for releasing packages. If not specified, the branch + * @param {string} [releaseBranch] A name of the branch that should be used for releasing packages. If not specified, the branch * used for the main repository will be used. */ @@ -758,5 +762,5 @@ module.exports = async function generateChangelogForMonoRepository( options ) { * * @param {Date} now The current date. * - * @returns {String} The formatted date inserted into the changelog. + * @returns {string} The formatted date inserted into the changelog. */ diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/generatechangelogforsinglepackage.js b/packages/ckeditor5-dev-release-tools/lib/tasks/generatechangelogforsinglepackage.js index 660591680..4000fea04 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/generatechangelogforsinglepackage.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/generatechangelogforsinglepackage.js @@ -3,22 +3,23 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const { tools, logger } = require( '@ckeditor/ckeditor5-dev-utils' ); -const chalk = require( 'chalk' ); -const semver = require( 'semver' ); -const cli = require( '../utils/cli' ); -const changelogUtils = require( '../utils/changelog' ); -const displayCommits = require( '../utils/displaycommits' ); -const generateChangelog = require( '../utils/generatechangelog' ); -const getPackageJson = require( '../utils/getpackagejson' ); -const getNewVersionType = require( '../utils/getnewversiontype' ); -const getCommits = require( '../utils/getcommits' ); -const getWriterOptions = require( '../utils/getwriteroptions' ); -const { getRepositoryUrl } = require( '../utils/transformcommitutils' ); -const transformCommitFactory = require( '../utils/transformcommitfactory' ); +import fs from 'fs'; +import { tools, logger } from '@ckeditor/ckeditor5-dev-utils'; +import chalk from 'chalk'; +import semver from 'semver'; +import displayCommits from '../utils/displaycommits.js'; +import generateChangelog from '../utils/generatechangelog.js'; +import getPackageJson from '../utils/getpackagejson.js'; +import getNewVersionType from '../utils/getnewversiontype.js'; +import getCommits from '../utils/getcommits.js'; +import getWriterOptions from '../utils/getwriteroptions.js'; +import { getRepositoryUrl } from '../utils/transformcommitutils.js'; +import transformCommitFactory from '../utils/transformcommitfactory.js'; +import getFormattedDate from '../utils/getformatteddate.js'; +import saveChangelog from '../utils/savechangelog.js'; +import getChangelog from '../utils/getchangelog.js'; +import provideVersion from '../utils/provideversion.js'; +import { CHANGELOG_FILE, CHANGELOG_HEADER, CLI_INDENT_SIZE } from '../utils/constants.js'; const SKIP_GENERATE_CHANGELOG = 'Typed "skip" as a new version. Aborting.'; @@ -27,20 +28,22 @@ const SKIP_GENERATE_CHANGELOG = 'Typed "skip" as a new version. Aborting.'; * * If the package does not have any commit, the user has to confirm whether the changelog should be generated. * - * @param {Object} [options={}] Additional options. + * @param {object} [options={}] Additional options. * - * @param {Boolean} [options.skipLinks=false] If set on true, links to release or commits will be omitted. + * @param {boolean} [options.skipLinks=false] If set on true, links to release or commits will be omitted. * - * @param {String} [options.from] A commit or tag name that will be the first param of the range of commits to collect. + * @param {string} [options.from] A commit or tag name that will be the first param of the range of commits to collect. * - * @param {String} [options.releaseBranch='master'] A name of the branch that should be used for releasing packages. + * @param {string} [options.releaseBranch='master'] A name of the branch that should be used for releasing packages. + * + * @param {string} [options.mainBranch='master'] A name of the main branch in the repository. * * @param {FormatDateCallback} [options.formatDate] A callback allowing defining a custom format of the date inserted into the changelog. * If not specified, the default date matches the `YYYY-MM-DD` pattern. * * @returns {Promise} */ -module.exports = async function generateChangelogForSinglePackage( options = {} ) { +export default async function generateChangelogForSinglePackage( options = {} ) { const log = logger(); const pkgJson = getPackageJson(); @@ -52,7 +55,8 @@ module.exports = async function generateChangelogForSinglePackage( options = {} const commitOptions = { from: options.from ? options.from : 'v' + pkgJson.version, - releaseBranch: options.releaseBranch + releaseBranch: options.releaseBranch || 'master', + mainBranch: options.mainBranch || 'master' }; // Initial release. @@ -79,7 +83,7 @@ module.exports = async function generateChangelogForSinglePackage( options = {} displayCommits( allCommits, { indentLevel: 1 } ); - return cli.provideVersion( pkgJson.version, releaseType, { indentLevel: 1 } ); + return provideVersion( pkgJson.version, releaseType, { indentLevel: 1 } ); } ) .then( version => { if ( version === 'skip' ) { @@ -106,12 +110,12 @@ module.exports = async function generateChangelogForSinglePackage( options = {} isInternalRelease, skipCommitsLink: Boolean( options.skipLinks ), skipCompareLink: Boolean( options.skipLinks ), - date: options.formatDate ? options.formatDate( new Date() ) : changelogUtils.getFormattedDate() + date: options.formatDate ? options.formatDate( new Date() ) : getFormattedDate() }; - const writerOptions = getWriterOptions( { + const writerOptions = getWriterOptions( commit => { // We do not allow modifying the commit hash value by the generator itself. - hash: hash => hash + return commit; } ); const publicCommits = [ ...allCommits ] @@ -137,25 +141,25 @@ module.exports = async function generateChangelogForSinglePackage( options = {} .then( changesFromCommits => { logProcess( 'Saving changelog...' ); - if ( !fs.existsSync( changelogUtils.changelogFile ) ) { + if ( !fs.existsSync( CHANGELOG_FILE ) ) { logInfo( 'Changelog file does not exist. Creating...', { isWarning: true, indentLevel: 1 } ); - changelogUtils.saveChangelog( changelogUtils.changelogHeader ); + saveChangelog( CHANGELOG_HEADER ); } - let currentChangelog = changelogUtils.getChangelog(); + let currentChangelog = getChangelog(); // Remove header from current changelog. - currentChangelog = currentChangelog.replace( changelogUtils.changelogHeader, '' ); + currentChangelog = currentChangelog.replace( CHANGELOG_HEADER, '' ); // Concat header, new and current changelog. - let newChangelog = changelogUtils.changelogHeader + changesFromCommits + currentChangelog.trim(); + let newChangelog = CHANGELOG_HEADER + changesFromCommits + currentChangelog.trim(); newChangelog = newChangelog.trim() + '\n'; // Save the changelog. - changelogUtils.saveChangelog( newChangelog ); + saveChangelog( newChangelog ); - tools.shExec( `git add ${ changelogUtils.changelogFile }`, { verbosity: 'error' } ); + tools.shExec( `git add ${ CHANGELOG_FILE }`, { verbosity: 'error' } ); tools.shExec( 'git commit -m "Docs: Changelog. [skip ci]"', { verbosity: 'error' } ); logInfo( 'Saved.', { indentLevel: 1 } ); @@ -182,25 +186,25 @@ module.exports = async function generateChangelogForSinglePackage( options = {} } /** - * @param {String} message - * @param {Object} [options={}] - * @param {Number} [options.indentLevel=0] - * @param {Boolean} [options.startWithNewLine=false] Whether to append a new line before the message. - * @param {Boolean} [options.isWarning=false] Whether to use `warning` method instead of `log`. + * @param {string} message + * @param {object} [options={}] + * @param {number} [options.indentLevel=0] + * @param {boolean} [options.startWithNewLine=false] Whether to append a new line before the message. + * @param {boolean} [options.isWarning=false] Whether to use `warning` method instead of `log`. */ function logInfo( message, options = {} ) { const indentLevel = options.indentLevel || 0; const startWithNewLine = options.startWithNewLine || false; const method = options.isWarning ? 'warning' : 'info'; - log[ method ]( `${ startWithNewLine ? '\n' : '' }${ ' '.repeat( indentLevel * cli.INDENT_SIZE ) }` + message ); + log[ method ]( `${ startWithNewLine ? '\n' : '' }${ ' '.repeat( indentLevel * CLI_INDENT_SIZE ) }` + message ); } -}; +} /** * @callback FormatDateCallback * * @param {Date} now The current date. * - * @returns {String} The formatted date inserted into the changelog. + * @returns {string} The formatted date inserted into the changelog. */ diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/preparerepository.js b/packages/ckeditor5-dev-release-tools/lib/tasks/preparerepository.js index 048a5396b..79e09c372 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/preparerepository.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/preparerepository.js @@ -3,27 +3,25 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs-extra' ); -const glob = require( 'glob' ); -const upath = require( 'upath' ); +import fs from 'fs-extra'; +import { glob } from 'glob'; +import upath from 'upath'; /** * The goal is to prepare the release directory containing the packages we want to publish. * - * @param {Object} options - * @param {String} options.outputDirectory Relative path to the destination directory where packages will be stored. - * @param {String} [options.cwd] Root of the repository to prepare. `process.cwd()` by default. - * @param {String} [options.packagesDirectory] Relative path to a location of packages. + * @param {object} options + * @param {string} options.outputDirectory Relative path to the destination directory where packages will be stored. + * @param {string} [options.cwd] Root of the repository to prepare. `process.cwd()` by default. + * @param {string} [options.packagesDirectory] Relative path to a location of packages. * If specified, all of the found packages will be copied. - * @param {Array.} [options.packagesToCopy] List of packages that should be processed. + * @param {Array.} [options.packagesToCopy] List of packages that should be processed. * If not specified, all packages found in `packagesDirectory` are considered. * @param {RootPackageJson} [options.rootPackageJson] Object containing values to use in the created the `package.json` file. * If not specified, the root package will not be created. * @returns {Promise} */ -module.exports = async function prepareRepository( options ) { +export default async function prepareRepository( options ) { const { outputDirectory, packagesDirectory, @@ -70,12 +68,12 @@ module.exports = async function prepareRepository( options ) { } return Promise.all( copyPromises ); -}; +} /** - * @param {Object} packageJson - * @param {String} [packageJson.name] - * @param {Array.} [packageJson.files] + * @param {object} packageJson + * @param {string} [packageJson.name] + * @param {Array.} [packageJson.files] */ function validateRootPackage( packageJson ) { if ( !packageJson.name ) { @@ -88,10 +86,10 @@ function validateRootPackage( packageJson ) { } /** - * @param {Object} options - * @param {String} options.cwd + * @param {object} options + * @param {string} options.cwd * @param {RootPackageJson} options.rootPackageJson - * @param {String} options.outputDirectoryPath + * @param {string} options.outputDirectoryPath * @returns {Promise} */ async function processRootPackage( { cwd, rootPackageJson, outputDirectoryPath } ) { @@ -102,20 +100,21 @@ async function processRootPackage( { cwd, rootPackageJson, outputDirectoryPath } await fs.ensureDir( rootPackageOutputPath ); await fs.writeJson( pkgJsonOutputPath, rootPackageJson, { spaces: 2, EOL: '\n' } ); - return glob.sync( rootPackageJson.files ).map( absoluteFilePath => { - const relativeFilePath = upath.relative( cwd, absoluteFilePath ); - const absoluteFileOutputPath = upath.join( rootPackageOutputPath, relativeFilePath ); + return ( await glob( rootPackageJson.files ) ) + .map( absoluteFilePath => { + const relativeFilePath = upath.relative( cwd, absoluteFilePath ); + const absoluteFileOutputPath = upath.join( rootPackageOutputPath, relativeFilePath ); - return fs.copy( absoluteFilePath, absoluteFileOutputPath ); - } ); + return fs.copy( absoluteFilePath, absoluteFileOutputPath ); + } ); } /** - * @param {Object} options - * @param {String} options.cwd - * @param {String} options.packagesDirectory - * @param {String} options.outputDirectoryPath - * @param {Array.} [options.packagesToCopy] + * @param {object} options + * @param {string} options.cwd + * @param {string} options.packagesDirectory + * @param {string} options.outputDirectoryPath + * @param {Array.} [options.packagesToCopy] * @returns {Promise} */ async function processMonorepoPackages( { cwd, packagesDirectory, packagesToCopy, outputDirectoryPath } ) { @@ -142,9 +141,9 @@ async function processMonorepoPackages( { cwd, packagesDirectory, packagesToCopy } /** - * @typedef {Object} RootPackageJson + * @typedef {object} RootPackageJson * - * @param {String} options.rootPackageJson.name Name of the package. Required value. + * @param {string} options.rootPackageJson.name Name of the package. Required value. * - * @param {Array.} options.rootPackageJson.files Array containing a list of files or directories to copy. Required value. + * @param {Array.} options.rootPackageJson.files Array containing a list of files or directories to copy. Required value. */ diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/publishpackages.js b/packages/ckeditor5-dev-release-tools/lib/tasks/publishpackages.js index 256c70b48..e4aa9c5bf 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/publishpackages.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/publishpackages.js @@ -3,16 +3,14 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const upath = require( 'upath' ); -const { glob } = require( 'glob' ); -const assertNpmAuthorization = require( '../utils/assertnpmauthorization' ); -const assertPackages = require( '../utils/assertpackages' ); -const assertNpmTag = require( '../utils/assertnpmtag' ); -const assertFilesToPublish = require( '../utils/assertfilestopublish' ); -const executeInParallel = require( '../utils/executeinparallel' ); -const publishPackageOnNpmCallback = require( '../utils/publishpackageonnpmcallback' ); +import upath from 'upath'; +import { glob } from 'glob'; +import assertNpmAuthorization from '../utils/assertnpmauthorization.js'; +import assertPackages from '../utils/assertpackages.js'; +import assertNpmTag from '../utils/assertnpmtag.js'; +import assertFilesToPublish from '../utils/assertfilestopublish.js'; +import executeInParallel from '../utils/executeinparallel.js'; +import publishPackageOnNpmCallback from '../utils/publishpackageonnpmcallback.js'; /** * The purpose of the script is to validate the packages prepared for the release and then release them on npm. @@ -26,27 +24,27 @@ const publishPackageOnNpmCallback = require( '../utils/publishpackageonnpmcallba * When the validation for each package passes, packages are published on npm. Optional callback is called for confirmation whether to * continue. * - * @param {Object} options - * @param {String} options.packagesDirectory Relative path to a location of packages to release. - * @param {String} options.npmOwner The account name on npm, which should be used to publish the packages. + * @param {object} options + * @param {string} options.packagesDirectory Relative path to a location of packages to release. + * @param {string} options.npmOwner The account name on npm, which should be used to publish the packages. * @param {ListrTaskObject} options.listrTask An instance of `ListrTask`. * @param {AbortSignal|null} [options.signal=null] Signal to abort the asynchronous process. - * @param {String} [options.npmTag='staging'] The npm distribution tag. - * @param {Object.>|null} [options.optionalEntries=null] Specifies which entries from the `files` field in the + * @param {string} [options.npmTag='staging'] The npm distribution tag. + * @param {Object.>|null} [options.optionalEntries=null] Specifies which entries from the `files` field in the * `package.json` are optional. The key is a package name, and its value is an array of optional entries from the `files` field, for which * it is allowed not to match any file. The `options.optionalEntries` object may also contain the `default` key, which is used for all * packages that do not have own definition. - * @param {String} [options.confirmationCallback=null] An callback whose response decides to continue the publishing packages. Synchronous + * @param {string} [options.confirmationCallback=null] An callback whose response decides to continue the publishing packages. Synchronous * and asynchronous callbacks are supported. - * @param {Boolean} [options.requireEntryPoint=false] Whether to verify if packages to publish define an entry point. In other words, + * @param {boolean} [options.requireEntryPoint=false] Whether to verify if packages to publish define an entry point. In other words, * whether their `package.json` define the `main` field. - * @param {Array.} [options.optionalEntryPointPackages=[]] If the entry point validator is enabled (`requireEntryPoint=true`), + * @param {Array.} [options.optionalEntryPointPackages=[]] If the entry point validator is enabled (`requireEntryPoint=true`), * this array contains a list of packages that will not be checked. In other words, they do not have to define the entry point. - * @param {String} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. - * @param {Number} [options.concurrency=4] Number of CPUs that will execute the task. + * @param {string} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. + * @param {number} [options.concurrency=4] Number of CPUs that will execute the task. * @returns {Promise} */ -module.exports = async function publishPackages( options ) { +export default async function publishPackages( options ) { const { packagesDirectory, npmOwner, @@ -87,4 +85,4 @@ module.exports = async function publishPackages( options ) { concurrency } ); } -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/push.js b/packages/ckeditor5-dev-release-tools/lib/tasks/push.js index ff6f1aebe..451bbb699 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/push.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/push.js @@ -3,21 +3,19 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); -const shellEscape = require( 'shell-escape' ); +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import shellEscape from 'shell-escape'; /** * Push the local changes to a remote server. * - * @param {Object} options - * @param {String} options.releaseBranch A name of the branch that should be used for releasing packages. - * @param {String} options.version Name of tag connected with the release. - * @param {String} [options.cwd] Root of the repository to prepare. `process.cwd()` by default. + * @param {object} options + * @param {string} options.releaseBranch A name of the branch that should be used for releasing packages. + * @param {string} options.version Name of tag connected with the release. + * @param {string} [options.cwd] Root of the repository to prepare. `process.cwd()` by default. * @returns {Promise} */ -module.exports = async function push( options ) { +export default async function push( options ) { const { releaseBranch, version, @@ -27,4 +25,4 @@ module.exports = async function push( options ) { const command = `git push origin ${ shellEscape( [ releaseBranch ] ) } v${ shellEscape( [ version ] ) }`; return tools.shExec( command, { cwd, verbosity: 'error', async: true } ); -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/reassignnpmtags.js b/packages/ckeditor5-dev-release-tools/lib/tasks/reassignnpmtags.js index 68d22f3fb..1746dfc6a 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/reassignnpmtags.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/reassignnpmtags.js @@ -5,29 +5,27 @@ * For licensing, see LICENSE.md. */ -/* eslint-env node */ +import chalk from 'chalk'; +import columns from 'cli-columns'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import util from 'util'; +import shellEscape from 'shell-escape'; +import assertNpmAuthorization from '../utils/assertnpmauthorization.js'; +import { exec } from 'child_process'; -'use strict'; - -const chalk = require( 'chalk' ); -const columns = require( 'cli-columns' ); -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); -const util = require( 'util' ); -const shellEscape = require( 'shell-escape' ); -const exec = util.promisify( require( 'child_process' ).exec ); -const assertNpmAuthorization = require( '../utils/assertnpmauthorization' ); +const execPromise = util.promisify( exec ); /** * Used to switch the tags from `staging` to `latest` for specified array of packages. * Each operation will be retried up to 3 times in case of failure. * - * @param {Object} options - * @param {String} options.npmOwner User that is authorized to release packages. - * @param {String} options.version Specifies the version of packages to reassign the tags for. - * @param {Array.} options.packages Array of packages' names to reassign tags for. + * @param {object} options + * @param {string} options.npmOwner User that is authorized to release packages. + * @param {string} options.version Specifies the version of packages to reassign the tags for. + * @param {Array.} options.packages Array of packages' names to reassign tags for. * @returns {Promise} */ -module.exports = async function reassignNpmTags( { npmOwner, version, packages } ) { +export default async function reassignNpmTags( { npmOwner, version, packages } ) { const errors = []; const packagesSkipped = []; const packagesUpdated = []; @@ -39,7 +37,7 @@ module.exports = async function reassignNpmTags( { npmOwner, version, packages } const updateTagPromises = packages.map( async packageName => { const command = `npm dist-tag add ${ shellEscape( [ packageName ] ) }@${ shellEscape( [ version ] ) } latest`; - const updateLatestTagRetryable = retry( () => exec( command ) ); + const updateLatestTagRetryable = retry( () => execPromise( command ) ); await updateLatestTagRetryable() .then( response => { if ( response.stdout ) { @@ -78,19 +76,19 @@ module.exports = async function reassignNpmTags( { npmOwner, version, packages } console.log( chalk.bold.red( '🐛 Errors found:' ) ); errors.forEach( msg => console.log( `* ${ msg }` ) ); } -}; +} /** - * @param {String} message - * @returns {String} + * @param {string} message + * @returns {string} */ function trimErrorMessage( message ) { return message.replace( /npm ERR!.*\n/g, '' ).trim(); } /** - * @param {Function} callback - * @param {Number} times + * @param {function} callback + * @param {number} times * @returns {RetryCallback} */ function retry( callback, times = 3 ) { diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/updatedependencies.js b/packages/ckeditor5-dev-release-tools/lib/tasks/updatedependencies.js index ddd078be7..8e22c2a2f 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/updatedependencies.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/updatedependencies.js @@ -3,11 +3,9 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs-extra' ); -const { glob } = require( 'glob' ); -const upath = require( 'upath' ); +import fs from 'fs-extra'; +import { glob } from 'glob'; +import upath from 'upath'; /** * The purpose of this script is to update all eligible dependencies to a version specified in the `options.version`. The following packages @@ -19,19 +17,19 @@ const upath = require( 'upath' ); * The eligible dependencies are distinguished by the return value from the `options.shouldUpdateVersionCallback` function. Only if this * callback returns a truthy value for a given dependency, its version will be updated. * - * @param {Object} options - * @param {String} options.version Target version or a range version to which all eligible dependencies will be updated. + * @param {object} options + * @param {string} options.version Target version or a range version to which all eligible dependencies will be updated. * Examples: `1.0.0`, `^1.0.0`, etc. * @param {UpdateVersionCallback} options.shouldUpdateVersionCallback Callback function that decides whether to update a version * for a dependency. It receives a package name as an argument and should return a boolean value. * @param {UpdateDependenciesPackagesDirectoryFilter|null} [options.packagesDirectoryFilter=null] An optional callback allowing * filtering out directories/packages that should not be touched by the task. - * @param {String} [options.packagesDirectory] Relative path to a location of packages to update their dependencies. If not specified, + * @param {string} [options.packagesDirectory] Relative path to a location of packages to update their dependencies. If not specified, * only the root package is checked. - * @param {String} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. + * @param {string} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. * @returns {Promise} */ -module.exports = async function updateDependencies( options ) { +export default async function updateDependencies( options ) { const { version, packagesDirectory, @@ -59,14 +57,14 @@ module.exports = async function updateDependencies( options ) { await fs.writeJson( pkgJsonPath, pkgJson, { spaces: 2 } ); } -}; +} /** * Updates the version for each eligible dependency. * - * @param {String} version - * @param {Function} callback - * @param {Object} [dependencies] + * @param {string} version + * @param {function} callback + * @param {object} [dependencies] */ function updateVersion( version, callback, dependencies ) { if ( !dependencies ) { @@ -81,10 +79,10 @@ function updateVersion( version, callback, dependencies ) { } /** - * @param {String} cwd - * @param {Array.} globPatterns + * @param {string} cwd + * @param {Array.} globPatterns * @param {UpdateDependenciesPackagesDirectoryFilter|null} packagesDirectoryFilter - * @returns {Promise.>} + * @returns {Promise.>} */ async function getPackageJsonPaths( cwd, globPatterns, packagesDirectoryFilter ) { const globOptions = { @@ -105,15 +103,15 @@ async function getPackageJsonPaths( cwd, globPatterns, packagesDirectoryFilter ) /** * @callback UpdateVersionCallback * - * @param {String} packageName A package name. + * @param {string} packageName A package name. * - * @returns {Boolean} Whether to update (`true`) or ignore (`false`) bumping the package version. + * @returns {boolean} Whether to update (`true`) or ignore (`false`) bumping the package version. */ /** * @callback UpdateDependenciesPackagesDirectoryFilter * - * @param {String} packageJsonPath An absolute path to a `package.json` file. + * @param {string} packageJsonPath An absolute path to a `package.json` file. * - * @returns {Boolean} Whether to include (`true`) or skip (`false`) processing the given directory/package. + * @returns {boolean} Whether to include (`true`) or skip (`false`) processing the given directory/package. */ diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/updateversions.js b/packages/ckeditor5-dev-release-tools/lib/tasks/updateversions.js index 96010dbc7..bd1416b48 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/updateversions.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/updateversions.js @@ -3,13 +3,13 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import upath from 'upath'; +import fs from 'fs-extra'; +import { glob } from 'glob'; +import semver from 'semver'; +import checkVersionAvailability from '../utils/checkversionavailability.js'; -const { glob } = require( 'glob' ); -const fs = require( 'fs-extra' ); -const semver = require( 'semver' ); -const { normalizeTrim, toUnix, dirname, join } = require( 'upath' ); -const checkVersionAvailability = require( '../utils/checkversionavailability' ); +const { normalizeTrim, toUnix, dirname, join } = upath; /** * The purpose of the script is to update the version of a root package found in the current working @@ -22,16 +22,16 @@ const checkVersionAvailability = require( '../utils/checkversionavailability' ); * Exception: passing a version starting with the `0.0.0-nightly` string. It is used for publishing * a nightly release. * - * @param {Object} options - * @param {String} options.version Version to store in a `package.json` file under the `version` key. + * @param {object} options + * @param {string} options.version Version to store in a `package.json` file under the `version` key. * @param {UpdateVersionsPackagesDirectoryFilter|null} [options.packagesDirectoryFilter=null] An optional callback allowing filtering out * directories/packages that should not be touched by the task. - * @param {String} [options.packagesDirectory] Relative path to a location of packages to update. If not specified, + * @param {string} [options.packagesDirectory] Relative path to a location of packages to update. If not specified, * only the root package is checked. - * @param {String} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. + * @param {string} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. * @returns {Promise} */ -module.exports = async function updateVersions( options ) { +export default async function updateVersions( options ) { const { packagesDirectory, version, @@ -63,13 +63,13 @@ module.exports = async function updateVersions( options ) { pkgJson.version = version; await fs.writeJson( pkgJsonPath, pkgJson, { spaces: 2 } ); } -}; +} /** - * @param {String} cwd - * @param {Array.} globPatterns + * @param {string} cwd + * @param {Array.} globPatterns * @param {UpdateVersionsPackagesDirectoryFilter|null} packagesDirectoryFilter - * @returns {Promise.>} + * @returns {Promise.>} */ async function getPackageJsonPaths( cwd, globPatterns, packagesDirectoryFilter ) { const pkgJsonPaths = await glob( globPatterns, { @@ -86,8 +86,8 @@ async function getPackageJsonPaths( cwd, globPatterns, packagesDirectoryFilter ) } /** - * @param {String} packagesDirectory - * @returns {Promise.} + * @param {string} packagesDirectory + * @returns {Promise.} */ function readPackageJson( packagesDirectory ) { const packageJsonPath = join( packagesDirectory, 'package.json' ); @@ -96,8 +96,8 @@ function readPackageJson( packagesDirectory ) { } /** - * @param {String|null} packagesDirectory - * @returns {Array.} + * @param {string|null} packagesDirectory + * @returns {Array.} */ function getGlobPatterns( packagesDirectory ) { const patterns = [ 'package.json' ]; @@ -110,9 +110,9 @@ function getGlobPatterns( packagesDirectory ) { } /** - * @param {Array.} pkgJsonPaths - * @param {String|null} packagesDirectory - * @returns {Object} + * @param {Array.} pkgJsonPaths + * @param {string|null} packagesDirectory + * @returns {object} */ function getRandomPackagePath( pkgJsonPaths, packagesDirectory ) { const randomPkgJsonPaths = packagesDirectory ? @@ -128,8 +128,8 @@ function getRandomPackagePath( pkgJsonPaths, packagesDirectory ) { * * A nightly version is always considered as valid. * - * @param {String} newVersion - * @param {String} currentVersion + * @param {string} newVersion + * @param {string} currentVersion */ function checkIfVersionIsValid( newVersion, currentVersion ) { if ( newVersion.startsWith( '0.0.0-nightly' ) ) { @@ -144,7 +144,7 @@ function checkIfVersionIsValid( newVersion, currentVersion ) { /** * @callback UpdateVersionsPackagesDirectoryFilter * - * @param {String} packageJsonPath An absolute path to a `package.json` file. + * @param {string} packageJsonPath An absolute path to a `package.json` file. * - * @returns {Boolean} Whether to include (`true`) or skip (`false`) processing the given directory/package. + * @returns {boolean} Whether to include (`true`) or skip (`false`) processing the given directory/package. */ diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/verifypackagespublishedcorrectly.js b/packages/ckeditor5-dev-release-tools/lib/tasks/verifypackagespublishedcorrectly.js index eda72f483..a6d0a4a77 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/verifypackagespublishedcorrectly.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/verifypackagespublishedcorrectly.js @@ -3,24 +3,22 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const upath = require( 'upath' ); -const { glob } = require( 'glob' ); -const fs = require( 'fs-extra' ); -const { checkVersionAvailability } = require( '../utils/checkversionavailability' ); +import upath from 'upath'; +import { glob } from 'glob'; +import fs from 'fs-extra'; +import checkVersionAvailability from '../utils/checkversionavailability.js'; /** * Npm sometimes throws incorrect error 409 while publishing, while the package uploads correctly. * The purpose of the script is to validate if packages that threw 409 are uploaded correctly to npm. * - * @param {Object} options - * @param {String} options.packagesDirectory Relative path to a location of packages to release. - * @param {String} options.version Version of the current release. - * @param {Function} options.onSuccess Callback fired when function is successful. + * @param {object} options + * @param {string} options.packagesDirectory Relative path to a location of packages to release. + * @param {string} options.version Version of the current release. + * @param {function} options.onSuccess Callback fired when function is successful. * @returns {Promise} */ -module.exports = async function verifyPackagesPublishedCorrectly( options ) { +export default async function verifyPackagesPublishedCorrectly( options ) { const { packagesDirectory, version, onSuccess } = options; const packagesToVerify = await glob( upath.join( packagesDirectory, '*' ), { absolute: true } ); const errors = []; @@ -52,4 +50,4 @@ module.exports = async function verifyPackagesPublishedCorrectly( options ) { } onSuccess( 'All packages that returned 409 were uploaded correctly.' ); -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/abortcontroller.js b/packages/ckeditor5-dev-release-tools/lib/utils/abortcontroller.js index 0fcb83352..54c9617fe 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/abortcontroller.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/abortcontroller.js @@ -3,16 +3,12 @@ * For licensing, see LICENSE.md. */ -/* eslint-env node */ - -'use strict'; - /** * Creates an AbortController instance and registers the listener function on SIGINT event that aborts the asynchronous process. * * @returns {AbortController} */ -function registerAbortController() { +export function registerAbortController() { const abortController = new AbortController(); const listener = () => { @@ -33,15 +29,10 @@ function registerAbortController() { * * @param {AbortController} abortController */ -function deregisterAbortController( abortController ) { +export function deregisterAbortController( abortController = undefined ) { if ( !abortController || !abortController._listener ) { return; } process.removeListener( 'SIGINT', abortController._listener ); } - -module.exports = { - registerAbortController, - deregisterAbortController -}; diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/assertfilestopublish.js b/packages/ckeditor5-dev-release-tools/lib/utils/assertfilestopublish.js index 3ae0ebbff..9a261bca5 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/assertfilestopublish.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/assertfilestopublish.js @@ -3,20 +3,18 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs-extra' ); -const upath = require( 'upath' ); -const { glob } = require( 'glob' ); +import fs from 'fs-extra'; +import upath from 'upath'; +import { glob } from 'glob'; /** * Checks if all files expected to be released actually exist in the package directory. Verification takes place for all packages. * - * @param {String} packagePaths - * @param {Object.>|null} optionalEntries + * @param {string} packagePaths + * @param {Object.>|null} optionalEntries * @returns {Promise} */ -module.exports = async function assertFilesToPublish( packagePaths, optionalEntries ) { +export default async function assertFilesToPublish( packagePaths, optionalEntries = null ) { const errors = []; for ( const packagePath of packagePaths ) { @@ -59,15 +57,15 @@ module.exports = async function assertFilesToPublish( packagePaths, optionalEntr if ( errors.length ) { throw new Error( errors.join( '\n' ) ); } -}; +} /** * Filters out the optional entries from the `files` field and returns only the required ones. * - * @param {Array.} entries - * @param {String} packageName - * @param {Object.>|null} optionalEntries - * @returns {Array.} + * @param {Array.} entries + * @param {string} packageName + * @param {Object.>|null} optionalEntries + * @returns {Array.} */ function getRequiredEntries( entries, packageName, optionalEntries ) { if ( !optionalEntries ) { diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/assertnpmauthorization.js b/packages/ckeditor5-dev-release-tools/lib/utils/assertnpmauthorization.js index 498023670..da7768037 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/assertnpmauthorization.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/assertnpmauthorization.js @@ -3,17 +3,15 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); +import { tools } from '@ckeditor/ckeditor5-dev-utils'; /** * Checks whether a user is logged to npm as the provided account name. * - * @param {String} npmOwner Expected npm account name that should be logged into npm. + * @param {string} npmOwner Expected npm account name that should be logged into npm. * @returns {Promise} */ -module.exports = async function assertNpmAuthorization( npmOwner ) { +export default async function assertNpmAuthorization( npmOwner ) { return tools.shExec( 'npm whoami', { verbosity: 'error', async: true } ) .then( npmCurrentUser => { if ( npmOwner !== npmCurrentUser.trim() ) { @@ -23,4 +21,4 @@ module.exports = async function assertNpmAuthorization( npmOwner ) { .catch( () => { throw new Error( `You must be logged to npm as "${ npmOwner }" to execute this release step.` ); } ); -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/assertnpmtag.js b/packages/ckeditor5-dev-release-tools/lib/utils/assertnpmtag.js index e6ec54d8e..8c8aa60ca 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/assertnpmtag.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/assertnpmtag.js @@ -3,20 +3,18 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs-extra' ); -const upath = require( 'upath' ); -const semver = require( 'semver' ); +import fs from 'fs-extra'; +import upath from 'upath'; +import semver from 'semver'; /** * Checks if the npm tag matches the tag calculated from the package version. Verification takes place for all packages. * - * @param {Array.} packagePaths - * @param {String} npmTag + * @param {Array.} packagePaths + * @param {string} npmTag * @returns {Promise} */ -module.exports = async function assertNpmTag( packagePaths, npmTag ) { +export default async function assertNpmTag( packagePaths, npmTag ) { const errors = []; for ( const packagePath of packagePaths ) { @@ -38,7 +36,7 @@ module.exports = async function assertNpmTag( packagePaths, npmTag ) { if ( errors.length ) { throw new Error( errors.join( '\n' ) ); } -}; +} /** * Returns the version tag for the package. @@ -46,8 +44,8 @@ module.exports = async function assertNpmTag( packagePaths, npmTag ) { * For the official release, returns the "latest" tag. For a non-official release (pre-release), returns the version tag extracted from * the package version. * - * @param {String} version - * @returns {String} + * @param {string} version + * @returns {string} */ function getVersionTag( version ) { const [ versionTag ] = semver.prerelease( version ) || [ 'latest' ]; diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/assertpackages.js b/packages/ckeditor5-dev-release-tools/lib/utils/assertpackages.js index dbc5226e0..7fc39a05b 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/assertpackages.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/assertpackages.js @@ -3,23 +3,21 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs-extra' ); -const upath = require( 'upath' ); +import fs from 'fs-extra'; +import upath from 'upath'; /** * Checks if all packages in the provided directories contain the `package.json` file. * - * @param {Array.} packagePaths - * @param {Object} options - * @param {Boolean} options.requireEntryPoint Whether to verify if packages to publish define an entry point. In other words, + * @param {Array.} packagePaths + * @param {object} options + * @param {boolean} options.requireEntryPoint Whether to verify if packages to publish define an entry point. In other words, * whether their `package.json` define the `main` field. - * @param {Array.} options.optionalEntryPointPackages If the entry point validator is enabled (`requireEntryPoint=true`), + * @param {Array.} options.optionalEntryPointPackages If the entry point validator is enabled (`requireEntryPoint=true`), * this array contains a list of packages that will not be checked. In other words, they do not have to define the entry point. * @returns {Promise} */ -module.exports = async function assertPackages( packagePaths, options ) { +export default async function assertPackages( packagePaths, options ) { const errors = []; const { requireEntryPoint, optionalEntryPointPackages } = options; @@ -49,4 +47,4 @@ module.exports = async function assertPackages( packagePaths, options ) { if ( errors.length ) { throw new Error( errors.join( '\n' ) ); } -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/changelog.js b/packages/ckeditor5-dev-release-tools/lib/utils/changelog.js deleted file mode 100644 index 6f1fc4b59..000000000 --- a/packages/ckeditor5-dev-release-tools/lib/utils/changelog.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const { format } = require( 'date-fns' ); -const { getRepositoryUrl } = require( './transformcommitutils' ); - -const utils = { - /** - * Changelog file name. - */ - changelogFile: 'CHANGELOG.md', - - /** - * Changelog header. - */ - changelogHeader: 'Changelog\n=========\n\n', - - /** - * Retrieves changes from the changelog for the given version (tag). - * - * @param {String} version - * @param {String} [cwd=process.cwd()] Where to look for the changelog file. - * @returns {String|null} - */ - getChangesForVersion( version, cwd = process.cwd() ) { - version = version.replace( /^v/, '' ); - - const changelog = utils.getChangelog( cwd ).replace( utils.changelogHeader, '\n' ); - const match = changelog.match( new RegExp( `\\n(## \\[?${ version }\\]?[\\s\\S]+?)(?:\\n## \\[?|$)` ) ); - - if ( !match || !match[ 1 ] ) { - return null; - } - - return match[ 1 ].replace( /##[^\n]+\n/, '' ).trim(); - }, - - /** - * @param {String} [cwd=process.cwd()] Where to look for the changelog file. - * @returns {String|null} - */ - getChangelog( cwd = process.cwd() ) { - const changelogFile = path.join( cwd, utils.changelogFile ); - - if ( !fs.existsSync( changelogFile ) ) { - return null; - } - - return fs.readFileSync( changelogFile, 'utf-8' ); - }, - - /** - * @param {String} content - * @param {String} [cwd=process.cwd()] Where to look for the changelog file. - */ - saveChangelog( content, cwd = process.cwd() ) { - const changelogFile = path.join( cwd, utils.changelogFile ); - - fs.writeFileSync( changelogFile, content, 'utf-8' ); - }, - - /** - * @param {Number} length - * @param {String} [cwd=process.cwd()] Where to look for the changelog file. - */ - truncateChangelog( length, cwd = process.cwd() ) { - const changelog = utils.getChangelog( cwd ); - - if ( !changelog ) { - return; - } - - const entryHeader = '## [\\s\\S]+?'; - const entryHeaderRegexp = new RegExp( `\\n(${ entryHeader })(?=\\n${ entryHeader }|$)`, 'g' ); - - const entries = [ ...changelog.matchAll( entryHeaderRegexp ) ] - .filter( match => match && match[ 1 ] ) - .map( match => match[ 1 ] ); - - if ( !entries.length ) { - return; - } - - const truncatedEntries = entries.slice( 0, length ); - - const changelogFooter = entries.length > truncatedEntries.length ? - `\n\n---\n\nTo see all releases, visit the [release page](${ getRepositoryUrl( cwd ) }/releases).\n` : - '\n'; - - const truncatedChangelog = utils.changelogHeader + truncatedEntries.join( '\n' ).trim() + changelogFooter; - - utils.saveChangelog( truncatedChangelog, cwd ); - }, - - /** - * @returns {String} - */ - getFormattedDate() { - return format( new Date(), 'yyyy-MM-dd' ); - } -}; - -module.exports = utils; diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/checkversionavailability.js b/packages/ckeditor5-dev-release-tools/lib/utils/checkversionavailability.js index 547265df4..197257d71 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/checkversionavailability.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/checkversionavailability.js @@ -3,10 +3,8 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); -const shellEscape = require( 'shell-escape' ); +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import shellEscape from 'shell-escape'; /** * Checks if the provided version for the package exists in the npm registry. @@ -14,11 +12,11 @@ const shellEscape = require( 'shell-escape' ); * Returns a promise that resolves to `true` if the provided version does not exist or resolves the promise to `false` otherwise. * If the `npm show` command exits with an error, it is re-thrown. * - * @param {String} version - * @param {String} packageName + * @param {string} version + * @param {string} packageName * @returns {Promise} */ -module.exports = async function checkVersionAvailability( version, packageName ) { +export default async function checkVersionAvailability( version, packageName ) { const command = `npm show ${ shellEscape( [ packageName ] ) }@${ shellEscape( [ version ] ) } version`; return tools.shExec( command, { verbosity: 'silent', async: true } ) @@ -42,4 +40,4 @@ module.exports = async function checkVersionAvailability( version, packageName ) // Npm < 8.13.0 should never reach this line. return true; } ); -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/cli.js b/packages/ckeditor5-dev-release-tools/lib/utils/cli.js deleted file mode 100644 index 7fa0caed4..000000000 --- a/packages/ckeditor5-dev-release-tools/lib/utils/cli.js +++ /dev/null @@ -1,349 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const inquirer = require( 'inquirer' ); -const semver = require( 'semver' ); -const chalk = require( 'chalk' ); - -const QUESTION_MARK = chalk.cyan( '?' ); - -const cli = { - /** - * A size of default indent for a log. - */ - INDENT_SIZE: 3, - - /** - * A size of indent for a second and next lines in a log. The number is equal to length of the log string: - * '* 1234567 ', where '1234567' is a short commit id. - * It does not include a value from `cli.INDENT_SIZE`. - */ - COMMIT_INDENT_SIZE: 10, - - /** - * Asks a user for a confirmation for updating and tagging versions of the packages. - * - * @param {Map} packages Packages to release. - * @returns {Promise.} - */ - confirmUpdatingVersions( packages ) { - let message = 'Packages and their old and new versions:\n'; - - for ( const packageName of Array.from( packages.keys() ).sort() ) { - const packageDetails = packages.get( packageName ); - - message += ` * "${ packageName }": v${ packageDetails.previousVersion } => v${ packageDetails.version }\n`; - } - - message += 'Continue?'; - - const confirmQuestion = { - message, - type: 'confirm', - name: 'confirm', - default: true - }; - - return inquirer.prompt( [ confirmQuestion ] ) - .then( answers => answers.confirm ); - }, - - /** - * Asks a user for a confirmation for publishing changes. - * - * @param {Map} packages Packages to release. - * @returns {Promise.} - */ - confirmPublishing( packages ) { - let message = 'Services where the release will be created:\n'; - - for ( const packageName of Array.from( packages.keys() ).sort() ) { - const packageDetails = packages.get( packageName ); - - let packageMessage = ` * "${ packageName }" - version: ${ packageDetails.version }`; - - const services = []; - - if ( packageDetails.shouldReleaseOnNpm ) { - services.push( 'NPM' ); - } - - if ( packageDetails.shouldReleaseOnGithub ) { - services.push( 'GitHub' ); - } - - let color; - - if ( services.length ) { - color = chalk.magenta; - packageMessage += ` - services: ${ services.join( ', ' ) } `; - } else { - color = chalk.gray; - packageMessage += ' - nothing to release'; - } - - message += color( packageMessage ) + '\n'; - } - - message += 'Continue?'; - - const confirmQuestion = { - message, - type: 'confirm', - name: 'confirm', - default: true - }; - - return inquirer.prompt( [ confirmQuestion ] ) - .then( answers => answers.confirm ); - }, - - /** - * Asks a user for a confirmation for removing archives created by `npm pack` command. - * - * @returns {Promise.} - */ - confirmRemovingFiles() { - const confirmQuestion = { - message: 'Remove created archives?', - type: 'confirm', - name: 'confirm', - default: true - }; - - return inquirer.prompt( [ confirmQuestion ] ) - .then( answers => answers.confirm ); - }, - - /** - * Asks a user for a confirmation for including a package that does not contain all required files. - * - * @returns {Promise.} - */ - confirmIncludingPackage() { - const confirmQuestion = { - message: 'Package does not contain all required files to publish. Include this package in the release and continue?', - type: 'confirm', - name: 'confirm', - default: true - }; - - return inquirer.prompt( [ confirmQuestion ] ) - .then( answers => answers.confirm ); - }, - - /** - * Asks a user for providing the new version. - * - * @param {String} packageVersion - * @param {String|null} releaseTypeOrNewVersion - * @param {Object} [options] - * @param {Boolean} [options.disableInternalVersion=false] Whether to "internal" version is enabled. - * @param {Boolean} [options.disableSkipVersion=false] Whether to "skip" version is enabled. - * @param {Number} [options.indentLevel=0] The indent level. - * @returns {Promise.} - */ - provideVersion( packageVersion, releaseTypeOrNewVersion, options = {} ) { - const indentLevel = options.indentLevel || 0; - const suggestedVersion = getSuggestedVersion(); - - let message = 'Type the new version, "skip" or "internal"'; - - if ( options.disableInternalVersion ) { - message = 'Type the new version or "skip"'; - } - - message += ` (suggested: "${ suggestedVersion }", current: "${ packageVersion }"):`; - - const versionQuestion = { - type: 'input', - name: 'version', - default: suggestedVersion, - message, - - filter( input ) { - return input.trim(); - }, - - validate( input ) { - if ( !options.disableSkipVersion && input === 'skip' ) { - return true; - } - - if ( !options.disableInternalVersion && input === 'internal' ) { - return true; - } - - // TODO: Check whether provided version is available. - return semver.valid( input ) ? true : 'Please provide a valid version.'; - }, - - prefix: getPrefix( indentLevel ) - }; - - return inquirer.prompt( [ versionQuestion ] ) - .then( answers => answers.version ); - - function getSuggestedVersion() { - if ( !releaseTypeOrNewVersion || releaseTypeOrNewVersion === 'skip' ) { - return 'skip'; - } - - if ( semver.valid( releaseTypeOrNewVersion ) ) { - return releaseTypeOrNewVersion; - } - - if ( releaseTypeOrNewVersion === 'internal' ) { - return options.disableInternalVersion ? 'skip' : 'internal'; - } - - if ( semver.prerelease( packageVersion ) ) { - releaseTypeOrNewVersion = 'prerelease'; - } - - // If package's version is below the '1.0.0', bump the 'minor' instead of 'major' - if ( releaseTypeOrNewVersion === 'major' && semver.gt( '1.0.0', packageVersion ) ) { - return semver.inc( packageVersion, 'minor' ); - } - - return semver.inc( packageVersion, releaseTypeOrNewVersion ); - } - }, - - /** - * Asks a user for providing the new version for a major release. - * - * @param {String} version - * @param {String} foundPackage - * @param {String} bumpType - * @param {Object} [options={}] - * @param {Number} [options.indentLevel=0] The indent level. - * @returns {Promise.} - */ - provideNewVersionForMonoRepository( version, foundPackage, bumpType, options = {} ) { - const indentLevel = options.indentLevel || 0; - const suggestedVersion = semver.inc( version, bumpType ); - - const message = 'Type the new version ' + - `(current highest: "${ version }" found in "${ chalk.underline( foundPackage ) }", suggested: "${ suggestedVersion }"):`; - - const versionQuestion = { - type: 'input', - name: 'version', - default: suggestedVersion, - message, - - filter( input ) { - return input.trim(); - }, - - validate( input ) { - if ( !semver.valid( input ) ) { - return 'Please provide a valid version.'; - } - - return semver.gt( input, version ) ? true : `Provided version must be higher than "${ version }".`; - }, - prefix: getPrefix( indentLevel ) - }; - - return inquirer.prompt( [ versionQuestion ] ) - .then( answers => answers.version ); - }, - - /** - * Asks a user for providing the GitHub token. - * - * @returns {Promise.} - */ - provideToken() { - const tokenQuestion = { - type: 'password', - name: 'token', - message: 'Provide the GitHub token:', - validate( input ) { - return input.length === 40 ? true : 'Please provide a valid token.'; - } - }; - - return inquirer.prompt( [ tokenQuestion ] ) - .then( answers => answers.token ); - }, - - /** - * Asks a user for selecting services where packages will be released. - * - * If the user choices a GitHub, required token also has to be provided. - * - * @returns {Promise.} - */ - configureReleaseOptions() { - const options = {}; - - const servicesQuestion = { - type: 'checkbox', - name: 'services', - message: 'Select services where packages will be released:', - choices: [ - 'npm', - 'GitHub' - ], - default: [ - 'npm', - 'GitHub' - ] - }; - - return inquirer.prompt( [ servicesQuestion ] ) - .then( answers => { - options.npm = answers.services.includes( 'npm' ); - options.github = answers.services.includes( 'GitHub' ); - - if ( !options.github ) { - return options; - } - - return cli.provideToken() - .then( token => { - options.token = token; - - return options; - } ); - } ); - }, - - /** - * Asks a user for a confirmation for updating and tagging versions of the packages. - * - * @param {String} versionTag A version tag based on a package version specified in `package.json`. - * @param {String} npmTag A tag typed by the user when using the release tools. - * @returns {Promise.} - */ - confirmNpmTag( versionTag, npmTag ) { - const areVersionsEqual = versionTag === npmTag; - const color = areVersionsEqual ? chalk.magenta : chalk.red; - - // eslint-disable-next-line max-len - const message = `The next release bumps the "${ color( versionTag ) }" version. Should it be published to npm as "${ color( npmTag ) }"?`; - - const confirmQuestion = { - message, - type: 'confirm', - name: 'confirm', - default: areVersionsEqual - }; - - return inquirer.prompt( [ confirmQuestion ] ) - .then( answers => answers.confirm ); - } -}; - -module.exports = cli; - -function getPrefix( indent ) { - return ' '.repeat( indent * cli.INDENT_SIZE ) + QUESTION_MARK; -} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/configurereleaseoptions.js b/packages/ckeditor5-dev-release-tools/lib/utils/configurereleaseoptions.js new file mode 100644 index 000000000..dd52761c5 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/configurereleaseoptions.js @@ -0,0 +1,43 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import inquirer from 'inquirer'; +import provideToken from './providetoken.js'; + +/** + * Asks a user for selecting services where packages will be released. + * + * If the user choices a GitHub, required token also has to be provided. + * + * @returns {Promise.} + */ +export default async function configureReleaseOptions() { + const options = {}; + + const servicesQuestion = { + type: 'checkbox', + name: 'services', + message: 'Select services where packages will be released:', + choices: [ + 'npm', + 'GitHub' + ], + default: [ + 'npm', + 'GitHub' + ] + }; + + const answers = await inquirer.prompt( [ servicesQuestion ] ); + + options.npm = answers.services.includes( 'npm' ); + options.github = answers.services.includes( 'GitHub' ); + + if ( options.github ) { + options.token = await provideToken(); + } + + return options; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/confirmincludingpackage.js b/packages/ckeditor5-dev-release-tools/lib/utils/confirmincludingpackage.js new file mode 100644 index 000000000..5bdb7dfca --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/confirmincludingpackage.js @@ -0,0 +1,24 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import inquirer from 'inquirer'; + +/** + * Asks a user for a confirmation for including a package that does not contain all required files. + * + * @returns {Promise.} + */ +export default async function confirmIncludingPackage() { + const confirmQuestion = { + message: 'Package does not contain all required files to publish. Include this package in the release and continue?', + type: 'confirm', + name: 'confirm', + default: true + }; + + const { confirm } = await inquirer.prompt( [ confirmQuestion ] ); + + return confirm; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/confirmnpmtag.js b/packages/ckeditor5-dev-release-tools/lib/utils/confirmnpmtag.js new file mode 100644 index 000000000..3839eb650 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/confirmnpmtag.js @@ -0,0 +1,32 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import inquirer from 'inquirer'; +import chalk from 'chalk'; + +/** + * Asks a user for a confirmation for updating and tagging versions of the packages. + * + * @param {string} versionTag A version tag based on a package version specified in `package.json`. + * @param {string} npmTag A tag typed by the user when using the release tools. + * @returns {Promise.} + */ +export default function confirmNpmTag( versionTag, npmTag ) { + const areVersionsEqual = versionTag === npmTag; + const color = areVersionsEqual ? chalk.magenta : chalk.red; + + // eslint-disable-next-line max-len + const message = `The next release bumps the "${ color( versionTag ) }" version. Should it be published to npm as "${ color( npmTag ) }"?`; + + const confirmQuestion = { + message, + type: 'confirm', + name: 'confirm', + default: areVersionsEqual + }; + + return inquirer.prompt( [ confirmQuestion ] ) + .then( answers => answers.confirm ); +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/constants.js b/packages/ckeditor5-dev-release-tools/lib/utils/constants.js new file mode 100644 index 000000000..ef26cb2b4 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/constants.js @@ -0,0 +1,26 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * Changelog file name. + */ +export const CHANGELOG_FILE = 'CHANGELOG.md'; + +/** + * Changelog header. + */ +export const CHANGELOG_HEADER = 'Changelog\n=========\n\n'; + +/** + * A size of default indent for a log. + */ +export const CLI_INDENT_SIZE = 3; + +/** + * A size of indent for a second and next lines in a log. The number is equal to length of the log string: + * '* 1234567 ', where '1234567' is a short commit id. + * It does not include a value from `cli.INDENT_SIZE`. + */ +export const CLI_COMMIT_INDENT_SIZE = 10; diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/displaycommits.js b/packages/ckeditor5-dev-release-tools/lib/utils/displaycommits.js index caa117e8a..2df792c87 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/displaycommits.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/displaycommits.js @@ -3,25 +3,23 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const chalk = require( 'chalk' ); -const { logger } = require( '@ckeditor/ckeditor5-dev-utils' ); -const utils = require( './transformcommitutils' ); -const { INDENT_SIZE, COMMIT_INDENT_SIZE } = require( './cli' ); +import chalk from 'chalk'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import * as utils from './transformcommitutils.js'; +import { CLI_COMMIT_INDENT_SIZE, CLI_INDENT_SIZE } from './constants.js'; /** * @param {Array.|Set.} commits - * @param {Object} [options={}] - * @param {Boolean} [options.attachLinkToCommit=false] Whether to attach a link to parsed commit. - * @param {Number} [options.indentLevel=1] The indent level. + * @param {object} [options={}] + * @param {boolean} [options.attachLinkToCommit=false] Whether to attach a link to parsed commit. + * @param {number} [options.indentLevel=1] The indent level. */ -module.exports = function displayCommits( commits, options = {} ) { +export default function displayCommits( commits, options = {} ) { const log = logger(); const attachLinkToCommit = options.attachLinkToCommit || false; const indentLevel = options.indentLevel || 1; - const listIndent = ' '.repeat( INDENT_SIZE * indentLevel ); + const listIndent = ' '.repeat( CLI_INDENT_SIZE * indentLevel ); if ( !( commits.length || commits.size ) ) { log.info( listIndent + chalk.italic( 'No commits to display.' ) ); @@ -30,7 +28,7 @@ module.exports = function displayCommits( commits, options = {} ) { const COMMITS_SEPARATOR = listIndent + chalk.gray( '-'.repeat( 112 ) ); // Group of commits by the commit's hash. - /** @type {Map.>} */ + /** @type {Map.>} */ const commitGroups = new Map(); for ( const singleCommit of commits ) { @@ -51,14 +49,14 @@ module.exports = function displayCommits( commits, options = {} ) { const isCommitIncluded = utils.availableCommitTypes.get( singleCommit.rawType ); const indent = commits.size > 1 ? listIndent.slice( 0, listIndent.length - 1 ) + chalk.gray( '|' ) : listIndent; - const noteIndent = indent + ' '.repeat( COMMIT_INDENT_SIZE ); + const noteIndent = indent + ' '.repeat( CLI_COMMIT_INDENT_SIZE ); let logMessage = `${ indent }* ${ chalk.yellow( hash.slice( 0, 7 ) ) } "${ utils.truncate( singleCommit.header, 100 ) }" `; if ( hasCorrectType && isCommitIncluded ) { logMessage += chalk.green( 'INCLUDED' ); } else if ( hasCorrectType && !isCommitIncluded ) { - logMessage += chalk.grey( 'SKIPPED' ); + logMessage += chalk.gray( 'SKIPPED' ); } else { logMessage += chalk.red( 'INVALID' ); } @@ -102,4 +100,4 @@ module.exports = function displayCommits( commits, options = {} ) { return commitGroups.get( commit.hash ); } -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/displayskippedpackages.js b/packages/ckeditor5-dev-release-tools/lib/utils/displayskippedpackages.js index edbba4657..490262323 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/displayskippedpackages.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/displayskippedpackages.js @@ -3,24 +3,22 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const chalk = require( 'chalk' ); -const { logger } = require( '@ckeditor/ckeditor5-dev-utils' ); -const getPackageJson = require( './getpackagejson' ); -const { INDENT_SIZE } = require( './cli' ); +import chalk from 'chalk'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import getPackageJson from './getpackagejson.js'; +import { CLI_INDENT_SIZE } from './constants.js'; /** * Displays skipped packages. * * @param {Set} skippedPackagesPaths */ -module.exports = function displaySkippedPackages( skippedPackagesPaths ) { +export default function displaySkippedPackages( skippedPackagesPaths ) { if ( !skippedPackagesPaths.size ) { return; } - const indent = ' '.repeat( INDENT_SIZE ); + const indent = ' '.repeat( CLI_INDENT_SIZE ); const packageNames = Array.from( skippedPackagesPaths ) .map( packagePath => getPackageJson( packagePath ).name ); @@ -29,4 +27,4 @@ module.exports = function displaySkippedPackages( skippedPackagesPaths ) { message += packageNames.map( line => indent + ` * ${ line }` ).join( '\n' ); logger().info( message ); -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/executeinparallel.js b/packages/ckeditor5-dev-release-tools/lib/utils/executeinparallel.js index 3ec2c208d..0ffac7661 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/executeinparallel.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/executeinparallel.js @@ -5,16 +5,15 @@ /* eslint-env node */ -'use strict'; +import crypto from 'crypto'; +import upath from 'upath'; +import os from 'os'; +import fs from 'fs/promises'; +import { Worker } from 'worker_threads'; +import { glob } from 'glob'; +import { registerAbortController, deregisterAbortController } from './abortcontroller.js'; -const crypto = require( 'crypto' ); -const upath = require( 'upath' ); -const fs = require( 'fs/promises' ); -const { Worker } = require( 'worker_threads' ); -const { glob } = require( 'glob' ); -const { registerAbortController, deregisterAbortController } = require( './abortcontroller' ); - -const WORKER_SCRIPT = upath.join( __dirname, 'parallelworker.cjs' ); +const WORKER_SCRIPT = new URL( './parallelworker.js', import.meta.url ); /** * This util allows executing a specified task in parallel using Workers. It can be helpful when executing a not resource-consuming @@ -24,20 +23,20 @@ const WORKER_SCRIPT = upath.join( __dirname, 'parallelworker.cjs' ); * Functions cannot be passed to workers. Hence, we store the callback as a Node.js file loaded by workers. * * @see https://nodejs.org/api/worker_threads.html - * @param {Object} options - * @param {String} options.packagesDirectory Relative path to a location of packages to execute a task. - * @param {Function} options.taskToExecute A callback that is executed on all found packages. + * @param {object} options + * @param {string} options.packagesDirectory Relative path to a location of packages to execute a task. + * @param {function} options.taskToExecute A callback that is executed on all found packages. * It receives an absolute path to a package as an argument. It can be synchronous or may return a promise. * @param {ListrTaskObject} [options.listrTask={}] An instance of `ListrTask`. * @param {AbortSignal|null} [options.signal=null] Signal to abort the asynchronous process. If not set, default AbortController is created. - * @param {Object} [options.taskOptions=null] Optional data required by the task. + * @param {object} [options.taskOptions=null] Optional data required by the task. * @param {ExecuteInParallelPackagesDirectoryFilter|null} [options.packagesDirectoryFilter=null] An optional callback allowing filtering out * directories/packages that should not be touched by the task. - * @param {String} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. - * @param {Number} [options.concurrency=require( 'os' ).cpus().length / 2] Number of CPUs that will execute the task. + * @param {string} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved. + * @param {number} [options.concurrency=require( 'os' ).cpus().length / 2] Number of CPUs that will execute the task. * @returns {Promise} */ -module.exports = async function executeInParallel( options ) { +export default async function executeInParallel( options ) { const { packagesDirectory, taskToExecute, @@ -46,9 +45,10 @@ module.exports = async function executeInParallel( options ) { taskOptions = null, packagesDirectoryFilter = null, cwd = process.cwd(), - concurrency = require( 'os' ).cpus().length / 2 + concurrency = os.cpus().length / 2 } = options; + const concurrencyAsInteger = Math.floor( concurrency ) || 1; const normalizedCwd = upath.toUnix( cwd ); const packages = ( await glob( `${ packagesDirectory }/*/`, { cwd: normalizedCwd, @@ -59,10 +59,10 @@ module.exports = async function executeInParallel( options ) { packages.filter( packagesDirectoryFilter ) : packages; - const packagesInThreads = getPackagesGroupedByThreads( packagesToProcess, concurrency ); + const packagesInThreads = getPackagesGroupedByThreads( packagesToProcess, concurrencyAsInteger ); - const callbackModule = upath.join( cwd, crypto.randomUUID() + '.cjs' ); - await fs.writeFile( callbackModule, `'use strict';\nmodule.exports = ${ taskToExecute };`, 'utf-8' ); + const callbackModule = upath.join( cwd, crypto.randomUUID() + '.mjs' ); + await fs.writeFile( callbackModule, `export default ${ taskToExecute };`, 'utf-8' ); const onPackageDone = progressFactory( listrTask, packagesToProcess.length ); @@ -96,11 +96,11 @@ module.exports = async function executeInParallel( options ) { deregisterAbortController( defaultAbortController ); } } ); -}; +} /** * @param {ListrTaskObject} listrTask - * @param {Number} total + * @param {number} total * @returns {Function} */ function progressFactory( listrTask, total ) { @@ -113,10 +113,10 @@ function progressFactory( listrTask, total ) { } /** - * @param {Object} options + * @param {object} options * @param {AbortSignal} options.signal - * @param {Function} options.onPackageDone - * @param {Object} options.workerData + * @param {function} options.onPackageDone + * @param {object} options.workerData * @returns {Promise} */ function createWorker( { signal, onPackageDone, workerData } ) { @@ -150,9 +150,9 @@ function createWorker( { signal, onPackageDone, workerData } ) { * * To avoid having packages with a common prefix in a single thread, use a loop for attaching packages to threads. * - * @param {Array.} packages An array of absolute paths to packages. - * @param {Number} concurrency A number of threads. - * @returns {Array.>} + * @param {Array.} packages An array of absolute paths to packages. + * @param {number} concurrency A number of threads. + * @returns {Array.>} */ function getPackagesGroupedByThreads( packages, concurrency ) { return packages.reduce( ( collection, packageItem, index ) => { @@ -169,19 +169,19 @@ function getPackagesGroupedByThreads( packages, concurrency ) { } /** - * @typedef {Object} ListrTaskObject + * @typedef {object} ListrTaskObject * * @see https://listr2.kilic.dev/api/classes/ListrTaskObject.html * - * @property {String} title Title of the task. + * @property {string} title Title of the task. * - * @property {String} output Update the current output of the task. + * @property {string} output Update the current output of the task. */ /** * @callback ExecuteInParallelPackagesDirectoryFilter * - * @param {String} directoryPath An absolute path to a directory. + * @param {string} directoryPath An absolute path to a directory. * - * @returns {Boolean} Whether to include (`true`) or skip (`false`) processing the given directory. + * @returns {boolean} Whether to include (`true`) or skip (`false`) processing the given directory. */ diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/generatechangelog.js b/packages/ckeditor5-dev-release-tools/lib/utils/generatechangelog.js index b0eb4ee60..8d7f39a67 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/generatechangelog.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/generatechangelog.js @@ -3,11 +3,9 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { Readable } = require( 'stream' ); -const { stream } = require( '@ckeditor/ckeditor5-dev-utils' ); -const conventionalChangelogWriter = require( 'conventional-changelog-writer' ); +import { Readable } from 'stream'; +import { stream } from '@ckeditor/ckeditor5-dev-utils'; +import { writeChangelogStream } from 'conventional-changelog-writer'; const UPDATED_TRANSLATION_COMMIT = '* Updated translations.'; @@ -16,29 +14,29 @@ const UPDATED_TRANSLATION_COMMIT = '* Updated translations.'; * * @param {Array.} commits * - * @param {Object} context - * @param {String} context.version Current version for the release. - * @param {String} context.repoUrl The repository URL. - * @param {String} context.currentTag A tag for the current version. - * @param {String} context.commit Commit keyword in the URL. - * @param {String} [context.previousTag] A tag for the previous version. - * @param {Boolean} [context.skipCommitsLink=false] Whether to skip adding links to commit. - * @param {Boolean} [context.skipCompareLink=false] Whether to remove the compare URL in the header. + * @param {object} context + * @param {string} context.version Current version for the release. + * @param {string} context.repoUrl The repository URL. + * @param {string} context.currentTag A tag for the current version. + * @param {string} context.commit Commit keyword in the URL. + * @param {string} [context.previousTag] A tag for the previous version. + * @param {boolean} [context.skipCommitsLink=false] Whether to skip adding links to commit. + * @param {boolean} [context.skipCompareLink=false] Whether to remove the compare URL in the header. * - * @param {Object} options - * @param {Object} options.transform - * @param {Function} options.transform.hash A function for mapping the commit's hash. - * @param {Array.|String} options.groupBy A key for grouping the commits. - * @param {Function} options.commitGroupsSort A sort function for the groups. - * @param {Function} options.noteGroupsSort A soft function for the notes. - * @param {String} options.mainTemplate The main template for the changelog. - * @param {String} options.headerPartial The "header" partial used in the main template. - * @param {String} options.commitPartial The "commit" partial used in the main template. - * @param {String} options.footerPartial The "footer" partial used in the main template. + * @param {object} options + * @param {object} options.transform + * @param {function} options.transform.hash A function for mapping the commit's hash. + * @param {Array.|string} options.groupBy A key for grouping the commits. + * @param {function} options.commitGroupsSort A sort function for the groups. + * @param {function} options.noteGroupsSort A soft function for the notes. + * @param {string} options.mainTemplate The main template for the changelog. + * @param {string} options.headerPartial The "header" partial used in the main template. + * @param {string} options.commitPartial The "commit" partial used in the main template. + * @param {string} options.footerPartial The "footer" partial used in the main template. * - * @returns {Promise.} + * @returns {Promise.} */ -module.exports = function generateChangelog( commits, context, options ) { +export default function generateChangelog( commits, context, options ) { const commitStream = new Readable( { objectMode: true } ); /* istanbul ignore next */ commitStream._read = function() {}; @@ -51,7 +49,7 @@ module.exports = function generateChangelog( commits, context, options ) { return new Promise( ( resolve, reject ) => { commitStream - .pipe( conventionalChangelogWriter( context, options ) ) + .pipe( writeChangelogStream( context, options ) ) .pipe( stream.noop( changes => { changes = mergeUpdateTranslationsCommits( changes.toString(), { skipCommitsLink: context.skipCommitsLink @@ -61,15 +59,15 @@ module.exports = function generateChangelog( commits, context, options ) { } ) ) .on( 'error', reject ); } ); -}; +} /** * Merges multiple "Updated translations." entries into the single commit. * - * @param {String} changelog Generated changelog. - * @param {Object} [options={}] - * @param {Boolean} [options.skipCommitsLink=false] Whether to skip adding links to commit. - * @returns {String} + * @param {string} changelog Generated changelog. + * @param {object} [options={}] + * @param {boolean} [options.skipCommitsLink=false] Whether to skip adding links to commit. + * @returns {string} */ function mergeUpdateTranslationsCommits( changelog, options = {} ) { let foundUpdatedTranslationCommit = false; diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getchangedfilesforcommit.js b/packages/ckeditor5-dev-release-tools/lib/utils/getchangedfilesforcommit.js index 311a2f36d..77a4dfc8d 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/getchangedfilesforcommit.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getchangedfilesforcommit.js @@ -3,17 +3,15 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); +import { tools } from '@ckeditor/ckeditor5-dev-utils'; /** * Returns an array with paths to changed files for given commit. * - * @param {String} commitId - * @returns {Array.} + * @param {string} commitId + * @returns {Array.} */ -module.exports = function getChangedFilesForCommit( commitId ) { +export default function getChangedFilesForCommit( commitId ) { const gitCommand = `git log -m -1 --name-only --pretty="format:" ${ commitId }`; const changedFiles = tools.shExec( gitCommand, { verbosity: 'error' } ).trim(); @@ -30,4 +28,4 @@ module.exports = function getChangedFilesForCommit( commitId ) { .split( '\n' ) .map( file => file.trim() ) .filter( item => item ); -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getchangelog.js b/packages/ckeditor5-dev-release-tools/lib/utils/getchangelog.js new file mode 100644 index 000000000..1425db472 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getchangelog.js @@ -0,0 +1,22 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import fs from 'fs'; +import path from 'path'; +import { CHANGELOG_FILE } from './constants.js'; + +/** + * @param {string} [cwd=process.cwd()] Where to look for the changelog file. + * @returns {string|null} + */ +export default function getChangelog( cwd = process.cwd() ) { + const changelogFile = path.join( cwd, CHANGELOG_FILE ); + + if ( !fs.existsSync( changelogFile ) ) { + return null; + } + + return fs.readFileSync( changelogFile, 'utf-8' ); +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getchangesforversion.js b/packages/ckeditor5-dev-release-tools/lib/utils/getchangesforversion.js new file mode 100644 index 000000000..4ba9f9f29 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getchangesforversion.js @@ -0,0 +1,28 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { CHANGELOG_HEADER } from './constants.js'; +import getChangelog from './getchangelog.js'; + +/** + * Retrieves changes from the changelog for the given version (tag). + * + * @param {string} version + * @param {string} [cwd=process.cwd()] Where to look for the changelog file. + * @returns {string|null} + */ +export default function getChangesForVersion( version, cwd = process.cwd() ) { + version = version.replace( /^v/, '' ); + + const changelog = getChangelog( cwd ).replace( CHANGELOG_HEADER, '\n' ); + + const match = changelog.match( new RegExp( `\\n(## \\[?${ version }\\]?[\\s\\S]+?)(?:\\n## \\[?|$)` ) ); + + if ( !match || !match[ 1 ] ) { + return null; + } + + return match[ 1 ].replace( /##[^\n]+\n/, '' ).trim(); +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getcommits.js b/packages/ckeditor5-dev-release-tools/lib/utils/getcommits.js index e92fd06f6..d95d605bd 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/getcommits.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getcommits.js @@ -3,26 +3,25 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const conventionalCommitsParser = require( 'conventional-commits-parser' ); -const conventionalCommitsFilter = require( 'conventional-commits-filter' ); -const gitRawCommits = require( 'git-raw-commits' ); -const concat = require( 'concat-stream' ); -const parserOptions = require( './parseroptions' ); -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); +import { parseCommitsStream } from 'conventional-commits-parser'; +import { filterRevertedCommitsSync } from 'conventional-commits-filter'; +import { getRawCommitsStream } from 'git-raw-commits'; +import concat from 'concat-stream'; +import parserOptions from './parseroptions.js'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import shellEscape from 'shell-escape'; /** * Returns a promise that resolves an array of commits since the last tag specified as `options.from`. * - * @param {Function} transformCommit - * @param {Object} options - * @param {String} [options.from] A commit or tag name that will be the first param of the range of commits to collect. - * @param {String} [options.releaseBranch='master'] A name of the branch that should be used for releasing packages. - * @param {String} [options.mainBranch='master'] A name of the main branch in the repository. + * @param {function} transformCommit + * @param {object} options + * @param {string} [options.from] A commit or tag name that will be the first param of the range of commits to collect. + * @param {string} [options.releaseBranch='master'] A name of the branch that should be used for releasing packages. + * @param {string} [options.mainBranch='master'] A name of the main branch in the repository. * @returns {Promise.>} */ -module.exports = function getCommits( transformCommit, options = {} ) { +export default function getCommits( transformCommit, options = {} ) { const releaseBranch = options.releaseBranch || 'master'; const mainBranch = options.mainBranch || 'master'; @@ -41,13 +40,13 @@ module.exports = function getCommits( transformCommit, options = {} ) { } else { // Otherwise, (release branch is other than the main branch) we need to merge arrays of commits. // See: https://github.com/ckeditor/ckeditor5/issues/7492. - const baseCommit = exec( `git merge-base ${ releaseBranch } ${ mainBranch }` ).trim(); + const baseCommit = exec( `git merge-base ${ shellEscape( [ releaseBranch, mainBranch ] ) }` ).trim(); const commitPromises = [ // 1. Commits from the last release and to the point where the release branch was created (the merge-base commit). findCommits( { from: options.from, to: baseCommit } ), // 2. Commits from the merge-base commit to HEAD. - findCommits( { from: baseCommit } ) + findCommits( { from: baseCommit, to: 'HEAD' } ) ]; return Promise.all( commitPromises ) @@ -62,7 +61,7 @@ module.exports = function getCommits( transformCommit, options = {} ) { } ); return new Promise( ( resolve, reject ) => { - const stream = gitRawCommits( gitRawCommitsOpts ) + const stream = getRawCommitsStream( gitRawCommitsOpts ) .on( 'error', err => { /* istanbul ignore else */ if ( err.message.match( /'HEAD': unknown/ ) ) { @@ -76,9 +75,9 @@ module.exports = function getCommits( transformCommit, options = {} ) { } } ); - stream.pipe( conventionalCommitsParser( parserOptions ) ) + stream.pipe( parseCommitsStream( parserOptions ) ) .pipe( concat( data => { - const commits = conventionalCommitsFilter( data ) + const commits = [ ...filterRevertedCommitsSync( data ) ] .map( commit => transformCommit( commit ) ) .reduce( ( allCommits, commit ) => { if ( Array.isArray( commit ) ) { @@ -101,4 +100,4 @@ module.exports = function getCommits( transformCommit, options = {} ) { function exec( command ) { return tools.shExec( command, { verbosity: 'error' } ); } -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getformatteddate.js b/packages/ckeditor5-dev-release-tools/lib/utils/getformatteddate.js new file mode 100644 index 000000000..9d4659ecc --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getformatteddate.js @@ -0,0 +1,13 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { format } from 'date-fns'; + +/** + * @returns {string} + */ +export default function getFormattedDate() { + return format( new Date(), 'yyyy-MM-dd' ); +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getnewversiontype.js b/packages/ckeditor5-dev-release-tools/lib/utils/getnewversiontype.js index e0bcda677..1a7e88b51 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/getnewversiontype.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getnewversiontype.js @@ -3,15 +3,13 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /** * Proposes new version based on commits. * * @param {Array.} commits - * @returns {String|null} + * @returns {string|null} */ -module.exports = function getNewVersionType( commits ) { +export default function getNewVersionType( commits ) { // No commits = no changes. if ( !commits.length ) { return 'skip'; @@ -50,4 +48,4 @@ module.exports = function getNewVersionType( commits ) { } return 'patch'; -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getnpmtagfromversion.js b/packages/ckeditor5-dev-release-tools/lib/utils/getnpmtagfromversion.js index 2d5b8c488..b3a45becd 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/getnpmtagfromversion.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getnpmtagfromversion.js @@ -3,16 +3,14 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const semver = require( 'semver' ); +import semver from 'semver'; /** - * @param {String} version - * @returns {String} + * @param {string} version + * @returns {string} */ -module.exports = function getNpmTagFromVersion( version ) { +export default function getNpmTagFromVersion( version ) { const [ versionTag ] = semver.prerelease( version ) || [ 'latest' ]; return versionTag; -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getpackagejson.js b/packages/ckeditor5-dev-release-tools/lib/utils/getpackagejson.js index ffe67412a..d7edb01f9 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/getpackagejson.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getpackagejson.js @@ -3,10 +3,8 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const upath = require( 'upath' ); +import fs from 'fs'; +import upath from 'upath'; /** * Returns object from `package.json`. @@ -14,10 +12,10 @@ const upath = require( 'upath' ); * This function is helpful for testing the whole process. Allows mocking the file * instead of create the fixtures. * - * @param {String} [cwd=process.cwd()] Where to look for package.json. - * @returns {Object} + * @param {string} [cwd=process.cwd()] Where to look for package.json. + * @returns {object} */ -module.exports = function getPackageJson( cwd = process.cwd() ) { +export default function getPackageJson( cwd = process.cwd() ) { let pkgJsonPath = cwd; if ( !pkgJsonPath.endsWith( 'package.json' ) ) { @@ -25,4 +23,4 @@ module.exports = function getPackageJson( cwd = process.cwd() ) { } return JSON.parse( fs.readFileSync( pkgJsonPath, 'utf-8' ) ); -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getpackagespaths.js b/packages/ckeditor5-dev-release-tools/lib/utils/getpackagespaths.js index 7d72d06d6..5467e18fc 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/getpackagespaths.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getpackagespaths.js @@ -3,32 +3,30 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const minimatch = require( 'minimatch' ); -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); -const getPackageJson = require( './getpackagejson' ); +import path from 'path'; +import { minimatch } from 'minimatch'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import getPackageJson from './getpackagejson.js'; /** * Returns an object with two collections of paths to packages which are located in single repository. * Those packages must be defined as dependencies in the repository found in `options.cwd`. * * - The first one is marked as `matched` and means that packages specified in a path (which is a combination of values specified as - * `options.cwd` and `options.packages`) match to given criteria. + * `options.cwd` and `options.packages`) match to given criteria. * - The second one is marked as `skipped` and means that packages should not be processed. They were listed as packages to skip * (`options.skipPackages` or don't mach to `options.scope`). * - * @param {Object} options - * @param {String} options.cwd Current work directory. - * @param {String|null} options.packages Name of directory where to look for packages. If `null`, only repository specified under + * @param {object} options + * @param {string} options.cwd Current work directory. + * @param {string|null} options.packages Name of directory where to look for packages. If `null`, only repository specified under * `options.cwd` will be returned. - * @param {String|Array.} options.skipPackages Glob pattern(s) which describes which packages should be skipped. - * @param {String} [options.scope] Package names have to match to specified glob pattern. - * @param {Boolean} [options.skipMainRepository=false] If set on true, package found in `options.cwd` will be skipped. + * @param {string|Array.} options.skipPackages Glob pattern(s) which describes which packages should be skipped. + * @param {string} [options.scope] Package names have to match to specified glob pattern. + * @param {boolean} [options.skipMainRepository=false] If set on true, package found in `options.cwd` will be skipped. * @returns {PathsCollection} */ -module.exports = function getPackagesPaths( options ) { +export default function getPackagesPaths( options ) { const pathsCollection = { matched: new Set(), skipped: new Set() @@ -79,12 +77,12 @@ module.exports = function getPackagesPaths( options ) { return true; } -}; +} /** - * @typedef {Object} PathsCollection + * @typedef {object} PathsCollection * - * @property {Set.} matched Packages that match given criteria. + * @property {Set.} matched Packages that match given criteria. * - * @property {Set.} skipped Packages that do not match given criteria. + * @property {Set.} skipped Packages that do not match given criteria. */ diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/getwriteroptions.js b/packages/ckeditor5-dev-release-tools/lib/utils/getwriteroptions.js index bdc5918a0..0773bfd19 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/getwriteroptions.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/getwriteroptions.js @@ -3,18 +3,21 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { getTypeOrder } from './transformcommitutils.js'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); -const fs = require( 'fs' ); -const path = require( 'path' ); const templatePath = path.join( __dirname, '..', 'templates' ); -const { getTypeOrder } = require( './transformcommitutils' ); /** - * @param {Function|Object} transform - * @returns {Object} + * @param {WriterOptionsTransformCallback} transform + * @returns {object} */ -module.exports = function getWriterOptions( transform ) { +export default function getWriterOptions( transform ) { return { transform, groupBy: 'type', @@ -26,8 +29,14 @@ module.exports = function getWriterOptions( transform ) { commitPartial: fs.readFileSync( path.join( templatePath, 'commit.hbs' ), 'utf-8' ), footerPartial: fs.readFileSync( path.join( templatePath, 'footer.hbs' ), 'utf-8' ) }; -}; +} function sortFunction( a, b ) { return getTypeOrder( a.title ) - getTypeOrder( b.title ); } + +/** + * @callback WriterOptionsTransformCallback + * @param {Commit} + * @returns {Commit} + */ diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/isversionpublishablefortag.js b/packages/ckeditor5-dev-release-tools/lib/utils/isversionpublishablefortag.js index 2b9a13856..888cf46cf 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/isversionpublishablefortag.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/isversionpublishablefortag.js @@ -3,19 +3,19 @@ * For licensing, see LICENSE.md. */ -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); -const semver = require( 'semver' ); -const shellEscape = require( 'shell-escape' ); +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import semver from 'semver'; +import shellEscape from 'shell-escape'; /** * This util aims to verify if the given `packageName` can be published with the given `version` on the `npmTag`. * - * @param {String} packageName - * @param {String} version - * @param {String} npmTag - * @return {Promise.} + * @param {string} packageName + * @param {string} version + * @param {string} npmTag + * @returns {Promise.} */ -module.exports = async function isVersionPublishableForTag( packageName, version, npmTag ) { +export default async function isVersionPublishableForTag( packageName, version, npmTag ) { const command = `npm view ${ shellEscape( [ packageName ] ) }@${ shellEscape( [ npmTag ] ) } version --silent`; const npmVersion = await tools.shExec( command, { async: true, verbosity: 'silent' } ) .then( value => value.trim() ) @@ -27,4 +27,4 @@ module.exports = async function isVersionPublishableForTag( packageName, version } return true; -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/parallelworker.cjs b/packages/ckeditor5-dev-release-tools/lib/utils/parallelworker.js similarity index 69% rename from packages/ckeditor5-dev-release-tools/lib/utils/parallelworker.cjs rename to packages/ckeditor5-dev-release-tools/lib/utils/parallelworker.js index 49b6adb4e..d7491b852 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/parallelworker.cjs +++ b/packages/ckeditor5-dev-release-tools/lib/utils/parallelworker.js @@ -3,18 +3,17 @@ * For licensing, see LICENSE.md. */ -'use strict'; - // This file is covered by the "executeInParallel() - integration" test cases. +import { parentPort, workerData } from 'worker_threads'; + // Required due to top-level await. ( async () => { /** - * @param {String} callbackModule - * @param {Array.} packages + * @param {string} callbackModule + * @param {Array.} packages */ - const { parentPort, workerData } = require( 'worker_threads' ); - const callback = require( workerData.callbackModule ); + const { default: callback } = await import( workerData.callbackModule ); for ( const packagePath of workerData.packages ) { await callback( packagePath, workerData.taskOptions ); diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/parseroptions.js b/packages/ckeditor5-dev-release-tools/lib/utils/parseroptions.js index a7d6ed298..ff54cac4b 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/parseroptions.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/parseroptions.js @@ -3,9 +3,7 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -module.exports = { +export default { mergePattern: /^Merge .*$/, headerPattern: /^([^:]+): (.*)$/, headerCorrespondence: [ diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/providenewversionformonorepository.js b/packages/ckeditor5-dev-release-tools/lib/utils/providenewversionformonorepository.js new file mode 100644 index 000000000..dee9fcec4 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/providenewversionformonorepository.js @@ -0,0 +1,51 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import inquirer from 'inquirer'; +import semver from 'semver'; +import chalk from 'chalk'; +import { CLI_INDENT_SIZE } from './constants.js'; + +/** + * Asks a user for providing the new version for a major release. + * + * @param {string} version + * @param {string} foundPackage + * @param {string} bumpType + * @param {object} [options={}] + * @param {number} [options.indentLevel=0] The indent level. + * @returns {Promise.} + */ +export default async function provideNewVersionForMonoRepository( version, foundPackage, bumpType, options = {} ) { + const indentLevel = options.indentLevel || 0; + const suggestedVersion = semver.inc( version, bumpType ); + + const message = 'Type the new version ' + + `(current highest: "${ version }" found in "${ chalk.underline( foundPackage ) }", suggested: "${ suggestedVersion }"):`; + + const versionQuestion = { + type: 'input', + name: 'version', + default: suggestedVersion, + message, + + filter( input ) { + return input.trim(); + }, + + validate( input ) { + if ( !semver.valid( input ) ) { + return 'Please provide a valid version.'; + } + + return semver.gt( input, version ) ? true : `Provided version must be higher than "${ version }".`; + }, + prefix: ' '.repeat( indentLevel * CLI_INDENT_SIZE ) + chalk.cyan( '?' ) + }; + + const answers = await inquirer.prompt( [ versionQuestion ] ); + + return answers.version; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/providetoken.js b/packages/ckeditor5-dev-release-tools/lib/utils/providetoken.js new file mode 100644 index 000000000..d3ae24c7f --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/providetoken.js @@ -0,0 +1,26 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import inquirer from 'inquirer'; + +/** + * Asks a user for providing the GitHub token. + * + * @returns {Promise.} + */ +export default async function provideToken() { + const tokenQuestion = { + type: 'password', + name: 'token', + message: 'Provide the GitHub token:', + validate( input ) { + return input.length === 40 ? true : 'Please provide a valid token.'; + } + }; + + const { token } = await inquirer.prompt( [ tokenQuestion ] ); + + return token; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/provideversion.js b/packages/ckeditor5-dev-release-tools/lib/utils/provideversion.js new file mode 100644 index 000000000..4b7642dcf --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/provideversion.js @@ -0,0 +1,98 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import inquirer from 'inquirer'; +import semver from 'semver'; +import chalk from 'chalk'; +import { CLI_INDENT_SIZE } from './constants.js'; + +/** + * Asks a user for providing the new version. + * + * @param {string} packageVersion + * @param {string|null} releaseTypeOrNewVersion + * @param {object} [options] + * @param {boolean} [options.disableInternalVersion=false] Whether to "internal" version is enabled. + * @param {boolean} [options.disableSkipVersion=false] Whether to "skip" version is enabled. + * @param {number} [options.indentLevel=0] The indent level. + * @returns {Promise.} + */ +export default function provideVersion( packageVersion, releaseTypeOrNewVersion, options = {} ) { + const indentLevel = options.indentLevel || 0; + const suggestedVersion = getSuggestedVersion( { + packageVersion, + releaseTypeOrNewVersion, + disableInternalVersion: options.disableInternalVersion + } ); + + let message = 'Type the new version, "skip" or "internal"'; + + if ( options.disableInternalVersion ) { + message = 'Type the new version or "skip"'; + } + + message += ` (suggested: "${ suggestedVersion }", current: "${ packageVersion }"):`; + + const versionQuestion = { + type: 'input', + name: 'version', + default: suggestedVersion, + message, + + filter( input ) { + return input.trim(); + }, + + validate( input ) { + if ( !options.disableSkipVersion && input === 'skip' ) { + return true; + } + + if ( !options.disableInternalVersion && input === 'internal' ) { + return true; + } + + // TODO: Check whether provided version is available. + return semver.valid( input ) ? true : 'Please provide a valid version.'; + }, + + prefix: ' '.repeat( indentLevel * CLI_INDENT_SIZE ) + chalk.cyan( '?' ) + }; + + return inquirer.prompt( [ versionQuestion ] ) + .then( answers => answers.version ); +} + +/** + * @param {object} options + * @param {string} options.packageVersion + * @param {string|null} options.releaseTypeOrNewVersion + * @param {boolean} options.disableInternalVersion + * @returns {string} + */ +function getSuggestedVersion( { packageVersion, releaseTypeOrNewVersion, disableInternalVersion } ) { + if ( !releaseTypeOrNewVersion || releaseTypeOrNewVersion === 'skip' ) { + return 'skip'; + } + + if ( semver.valid( releaseTypeOrNewVersion ) ) { + return releaseTypeOrNewVersion; + } + + if ( releaseTypeOrNewVersion === 'internal' ) { + return disableInternalVersion ? 'skip' : 'internal'; + } + + if ( semver.prerelease( packageVersion ) ) { + releaseTypeOrNewVersion = 'prerelease'; + } + + // If package's version is below the '1.0.0', bump the 'minor' instead of 'major' + if ( releaseTypeOrNewVersion === 'major' && semver.gt( '1.0.0', packageVersion ) ) { + return semver.inc( packageVersion, 'minor' ); + } + + return semver.inc( packageVersion, releaseTypeOrNewVersion ); +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/publishpackageonnpmcallback.js b/packages/ckeditor5-dev-release-tools/lib/utils/publishpackageonnpmcallback.js index fb9b5dcb5..b7bb7683c 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/publishpackageonnpmcallback.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/publishpackageonnpmcallback.js @@ -3,30 +3,28 @@ * For licensing, see LICENSE.md. */ -/* eslint-env node */ - -'use strict'; - /** * Calls the npm command to publish the package. When a package is successfully published, it is removed from the filesystem. * - * @param {String} packagePath - * @param {Object} taskOptions - * @param {String} taskOptions.npmTag + * @param {string} packagePath + * @param {object} taskOptions + * @param {string} taskOptions.npmTag * @returns {Promise} */ -module.exports = async function publishPackageOnNpmCallback( packagePath, taskOptions ) { - const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); - const upath = require( 'upath' ); - const fs = require( 'fs-extra' ); +export default async function publishPackageOnNpmCallback( packagePath, taskOptions ) { + const { tools } = await import( '@ckeditor/ckeditor5-dev-utils' ); + const { default: fs } = await import( 'fs-extra' ); + const { default: path } = await import( 'upath' ); - const result = await tools.shExec( `npm publish --access=public --tag ${ taskOptions.npmTag }`, { + const options = { cwd: packagePath, async: true, verbosity: 'error' - } ) + }; + + const result = await tools.shExec( `npm publish --access=public --tag ${ taskOptions.npmTag }`, options ) .catch( e => { - const packageName = upath.basename( packagePath ); + const packageName = path.basename( packagePath ); if ( e.toString().includes( 'code E409' ) ) { return { shouldKeepDirectory: true }; @@ -38,4 +36,4 @@ module.exports = async function publishPackageOnNpmCallback( packagePath, taskOp if ( !result || !result.shouldKeepDirectory ) { await fs.remove( packagePath ); } -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/savechangelog.js b/packages/ckeditor5-dev-release-tools/lib/utils/savechangelog.js new file mode 100644 index 000000000..d4e1e3683 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/savechangelog.js @@ -0,0 +1,18 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import fs from 'fs'; +import path from 'path'; +import { CHANGELOG_FILE } from './constants.js'; + +/** + * @param {string} content + * @param {string} [cwd=process.cwd()] Where to look for the changelog file. + */ +export default function saveChangelog( content, cwd = process.cwd() ) { + const changelogFile = path.join( cwd, CHANGELOG_FILE ); + + fs.writeFileSync( changelogFile, content, 'utf-8' ); +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/transformcommitfactory.js b/packages/ckeditor5-dev-release-tools/lib/utils/transformcommitfactory.js index 5835ac426..e2d6ef55b 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/transformcommitfactory.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/transformcommitfactory.js @@ -3,11 +3,9 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { cloneDeepWith } = require( 'lodash' ); -const utils = require( './transformcommitutils' ); -const getChangedFilesForCommit = require( './getchangedfilesforcommit' ); +import { cloneDeepWith } from 'lodash-es'; +import * as utils from './transformcommitutils.js'; +import getChangedFilesForCommit from './getchangedfilesforcommit.js'; // Squash commit follows the pattern: "A pull request title (#{number})". const SQUASH_COMMIT_REGEXP = /^[\W\w]+ \(#\d+\)$/; @@ -22,16 +20,16 @@ const SQUASH_COMMIT_REGEXP = /^[\W\w]+ \(#\d+\)$/; * - normalizes notes (e.g. "MAJOR BREAKING CHANGE" will be replaced with "MAJOR BREAKING CHANGES"), * - the commit is always being returned. Even, if it should not be added to the changelog. * - * @param {Object} [options={}] - * @param {Boolean} [options.treatMajorAsMinorBreakingChange=false] If set on true, all "MAJOR BREAKING CHANGES" notes will be replaced + * @param {object} [options={}] + * @param {boolean} [options.treatMajorAsMinorBreakingChange=false] If set on true, all "MAJOR BREAKING CHANGES" notes will be replaced * with "MINOR BREAKING CHANGES". This behaviour is being disabled automatically if `options.useExplicitBreakingChangeGroups` is * set on `false` because all commits will be treated as "BREAKING CHANGES". - * @param {Boolean} [options.useExplicitBreakingChangeGroups] If set on `true`, notes from parsed commits will be grouped as + * @param {boolean} [options.useExplicitBreakingChangeGroups] If set on `true`, notes from parsed commits will be grouped as * "MINOR BREAKING CHANGES" and "MAJOR BREAKING CHANGES'. If set on `false` (by default), all breaking changes notes will be treated * as "BREAKING CHANGES". * @returns {TransformCommit} */ -module.exports = function transformCommitFactory( options = {} ) { +export default function transformCommitFactory( options = {} ) { return rawCommit => { const commit = transformCommit( rawCommit ); @@ -264,8 +262,8 @@ module.exports = function transformCommitFactory( options = {} ) { /** * Merges multiple "Closes #x" references as "Closes #x, #y.". * - * @param {String} subject - * @returns {String} + * @param {string} subject + * @returns {string} */ function mergeCloseReferences( subject ) { const refs = []; @@ -355,8 +353,8 @@ module.exports = function transformCommitFactory( options = {} ) { * * For commits with no scope, `null` will be returned instead of the array (as `scope`). * - * @param {String} type First line from the commit message. - * @returns {Object} + * @param {string} type First line from the commit message. + * @returns {object} */ function extractScopeFromPrefix( type ) { if ( !type ) { @@ -389,8 +387,8 @@ module.exports = function transformCommitFactory( options = {} ) { * * For notes with no scope, `null` will be returned instead of the array (as `scope`). * - * @param {String} text A text that describes a note. - * @returns {Object} + * @param {string} text A text that describes a note. + * @returns {object} */ function extractScopeFromNote( text ) { const scopeAsText = text.match( /\(([^)]+)\):/ ); @@ -473,7 +471,7 @@ module.exports = function transformCommitFactory( options = {} ) { /** * @param {Array.} commits - * @returns {Boolean} + * @returns {boolean} */ function isSquashMergeCommit( commits ) { const [ squashCommit ] = commits; @@ -484,7 +482,7 @@ module.exports = function transformCommitFactory( options = {} ) { return !!squashCommit.header.match( SQUASH_COMMIT_REGEXP ); } -}; +} /** * @callback TransformCommit @@ -493,41 +491,41 @@ module.exports = function transformCommitFactory( options = {} ) { */ /** - * @typedef {Object} Commit + * @typedef {object} Commit * - * @property {Boolean} isPublicCommit Whether the commit should be added in the changelog. + * @property {boolean} isPublicCommit Whether the commit should be added in the changelog. * - * @property {String} rawType Type of the commit without any modifications. + * @property {string} rawType Type of the commit without any modifications. * - * @property {String|null} type Type of the commit (it can be modified). + * @property {string|null} type Type of the commit (it can be modified). * - * @property {String} header First line of the commit message. + * @property {string} header First line of the commit message. * - * @property {String} subject Subject of the commit. It's the header without the type. + * @property {string} subject Subject of the commit. It's the header without the type. * - * @property {Array.|null} scope Scope of the changes. + * @property {Array.|null} scope Scope of the changes. * - * @property {Array.} files A list of files tha the commit modified. + * @property {Array.} files A list of files tha the commit modified. * - * @property {String} hash The full commit SHA-1 id. + * @property {string} hash The full commit SHA-1 id. * - * @property {String} repositoryUrl The URL to the repository where the parsed commit has been done. + * @property {string} repositoryUrl The URL to the repository where the parsed commit has been done. ** - * @property {String|null} [body] Body of the commit message. + * @property {string|null} [body] Body of the commit message. * - * @property {String|null} [footer] Footer of the commit message. + * @property {string|null} [footer] Footer of the commit message. * * @property {Array.} [notes] Notes for the commit. * - * @property {Boolean} [skipCommitsLink] Whether to skip generating a URL to the commit by the generator. + * @property {boolean} [skipCommitsLink] Whether to skip generating a URL to the commit by the generator. */ /** - * @typedef {Object} CommitNote + * @typedef {object} CommitNote * - * @property {String} title Type of the note. + * @property {string} title Type of the note. * - * @property {String} text Text of the note. + * @property {string} text Text of the note. * - * @property {Array.} scope Scope of the note. + * @property {Array.} scope Scope of the note. */ diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/transformcommitutils.js b/packages/ckeditor5-dev-release-tools/lib/utils/transformcommitutils.js index b456c7cd4..fbd8b0e97 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/transformcommitutils.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/transformcommitutils.js @@ -3,161 +3,156 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const getPackageJson = require( './getpackagejson' ); - -const transformCommitUtils = { - /** - * A regexp for extracting additional changelog entries from the single commit. - * Prefixes of the commit must be synchronized the `getCommitType()` util. - */ - MULTI_ENTRIES_COMMIT_REGEXP: /(?:Feature|Other|Fix|Docs|Internal|Tests|Revert|Release)(?: \([\w\-, ]+?\))?:/g, - - /** - * Map of available types of the commits. - * Types marked as `false` will be ignored during generating the changelog. - */ - availableCommitTypes: new Map( [ - [ 'Fix', true ], - [ 'Feature', true ], - [ 'Other', true ], - - [ 'Docs', false ], - [ 'Internal', false ], - [ 'Tests', false ], - [ 'Revert', false ], - [ 'Release', false ] - ] ), - - /** - * Order of messages generated in changelog. - */ - typesOrder: { - 'Features': 1, - 'Bug fixes': 2, - 'Other changes': 3, - - 'MAJOR BREAKING CHANGES': 1, - 'MINOR BREAKING CHANGES': 2, - 'BREAKING CHANGES': 3 - }, - - /** - * Returns an order of a message in the changelog. - * - * @param {String} title - * @returns {Number} - */ - getTypeOrder( title ) { - for ( const typeTitle of Object.keys( transformCommitUtils.typesOrder ) ) { - if ( title.startsWith( typeTitle ) ) { - return transformCommitUtils.typesOrder[ typeTitle ]; - } +import getPackageJson from './getpackagejson.js'; + +/** + * A regexp for extracting additional changelog entries from the single commit. + * Prefixes of the commit must be synchronized the `getCommitType()` util. + */ +export const MULTI_ENTRIES_COMMIT_REGEXP = /(?:Feature|Other|Fix|Docs|Internal|Tests|Revert|Release)(?: \([\w\-, ]+?\))?:/g; + +/** + * Map of available types of the commits. + * Types marked as `false` will be ignored during generating the changelog. + */ +export const availableCommitTypes = new Map( [ + [ 'Fix', true ], + [ 'Feature', true ], + [ 'Other', true ], + + [ 'Docs', false ], + [ 'Internal', false ], + [ 'Tests', false ], + [ 'Revert', false ], + [ 'Release', false ] +] ); + +/** + * Order of messages generated in changelog. + */ +export const typesOrder = { + 'Features': 1, + 'Bug fixes': 2, + 'Other changes': 3, + + 'MAJOR BREAKING CHANGES': 1, + 'MINOR BREAKING CHANGES': 2, + 'BREAKING CHANGES': 3 +}; + +/** + * Returns an order of a message in the changelog. + * + * @param {string} title + * @returns {number} + */ +export function getTypeOrder( title ) { + for ( const typeTitle of Object.keys( typesOrder ) ) { + if ( title.startsWith( typeTitle ) ) { + return typesOrder[ typeTitle ]; } + } + + return 10; +} - return 10; - }, - - /** - * Replaces reference to the user (`@name`) with a link to the user's profile. - * - * @param {String} comment - * @returns {String} - */ - linkToGithubUser( comment ) { - return comment.replace( /(^|[\s(])@([\w-]+)(?![/\w-])/ig, ( matchedText, charBefore, nickName ) => { - return `${ charBefore }[@${ nickName }](https://github.com/${ nickName })`; - } ); - }, - - /** - * Replaces reference to issue (#ID) with a link to the issue. - * If comment matches to "organization/repository#ID", link will lead to the specified repository. - * - * @param {String} comment - * @returns {String} - */ - linkToGithubIssue( comment ) { - return comment.replace( /(\/?[\w-]+\/[\w-]+)?#([\d]+)(?=$|[\s,.)\]])/igm, ( matchedText, maybeRepository, issueId ) => { - if ( maybeRepository ) { - if ( maybeRepository.startsWith( '/' ) ) { - return matchedText; - } - - return `[${ maybeRepository }#${ issueId }](https://github.com/${ maybeRepository }/issues/${ issueId })`; +/** + * Replaces reference to the user (`@name`) with a link to the user's profile. + * + * @param {string} comment + * @returns {string} + */ +export function linkToGithubUser( comment ) { + return comment.replace( /(^|[\s(])@([\w-]+)(?![/\w-])/ig, ( matchedText, charBefore, nickName ) => { + return `${ charBefore }[@${ nickName }](https://github.com/${ nickName })`; + } ); +} + +/** + * Replaces reference to issue (#ID) with a link to the issue. + * If comment matches to "organization/repository#ID", link will lead to the specified repository. + * + * @param {string} comment + * @returns {string} + */ +export function linkToGithubIssue( comment ) { + return comment.replace( /(\/?[\w-]+\/[\w-]+)?#([\d]+)(?=$|[\s,.)\]])/igm, ( matchedText, maybeRepository, issueId ) => { + if ( maybeRepository ) { + if ( maybeRepository.startsWith( '/' ) ) { + return matchedText; } - const repositoryUrl = transformCommitUtils.getRepositoryUrl(); - - // But if doesn't, let's add it. - return `[#${ issueId }](${ repositoryUrl }/issues/${ issueId })`; - } ); - }, - - /** - * Changes a singular type of commit to plural which will be displayed in a changelog. - * - * The switch cases must be synchronized with the `MULTI_ENTRIES_COMMIT_REGEXP` regexp. - * - * @param {String} commitType - * @returns {String} - */ - getCommitType( commitType ) { - switch ( commitType ) { - case 'Feature': - return 'Features'; - - case 'Fix': - return 'Bug fixes'; - - case 'Other': - return 'Other changes'; - - default: - throw new Error( `Given invalid type of commit ("${ commitType }").` ); - } - }, - - /** - * @param {String} sentence - * @param {Number} length - * @returns {String} - */ - truncate( sentence, length ) { - if ( sentence.length <= length ) { - return sentence; + return `[${ maybeRepository }#${ issueId }](https://github.com/${ maybeRepository }/issues/${ issueId })`; } - return sentence.slice( 0, length - 3 ).trim() + '...'; - }, - - /** - * Returns a URL to the repository whether the commit is being parsed. - * - * @param {String} [cwd=process.cwd()] - * @returns {String} - */ - getRepositoryUrl( cwd = process.cwd() ) { - const packageJson = getPackageJson( cwd ); - - // Due to merging our issue trackers, `packageJson.bugs` will point to the same place for every package. - // We cannot rely on this value anymore. See: https://github.com/ckeditor/ckeditor5/issues/1988. - // Instead of we can take a value from `packageJson.repository` and adjust it to match to our requirements. - let repositoryUrl = ( typeof packageJson.repository === 'object' ) ? packageJson.repository.url : packageJson.repository; - - if ( !repositoryUrl ) { - throw new Error( `The package.json for "${ packageJson.name }" must contain the "repository" property.` ); - } + const repositoryUrl = getRepositoryUrl(); + + // But if doesn't, let's add it. + return `[#${ issueId }](${ repositoryUrl }/issues/${ issueId })`; + } ); +} + +/** + * Changes a singular type of commit to plural which will be displayed in a changelog. + * + * The switch cases must be synchronized with the `MULTI_ENTRIES_COMMIT_REGEXP` regexp. + * + * @param {string} commitType + * @returns {string} + */ +export function getCommitType( commitType ) { + switch ( commitType ) { + case 'Feature': + return 'Features'; - // If the value ends with ".git", we need to remove it. - repositoryUrl = repositoryUrl.replace( /\.git$/, '' ); + case 'Fix': + return 'Bug fixes'; - // Remove "/issues" suffix as well. - repositoryUrl = repositoryUrl.replace( /\/issues/, '' ); + case 'Other': + return 'Other changes'; - return repositoryUrl; + default: + throw new Error( `Given invalid type of commit ("${ commitType }").` ); } -}; +} + +/** + * @param {string} sentence + * @param {number} length + * @returns {string} + */ +export function truncate( sentence, length ) { + if ( sentence.length <= length ) { + return sentence; + } + + return sentence.slice( 0, length - 3 ).trim() + '...'; +} + +/** + * Returns a URL to the repository whether the commit is being parsed. + * + * @param {string} [cwd=process.cwd()] + * @returns {string} + */ +export function getRepositoryUrl( cwd = process.cwd() ) { + const packageJson = getPackageJson( cwd ); + + // Due to merging our issue trackers, `packageJson.bugs` will point to the same place for every package. + // We cannot rely on this value anymore. See: https://github.com/ckeditor/ckeditor5/issues/1988. + // Instead of we can take a value from `packageJson.repository` and adjust it to match to our requirements. + let repositoryUrl = ( typeof packageJson.repository === 'object' ) ? packageJson.repository.url : packageJson.repository; + + if ( !repositoryUrl ) { + throw new Error( `The package.json for "${ packageJson.name }" must contain the "repository" property.` ); + } + + // If the value ends with ".git", we need to remove it. + repositoryUrl = repositoryUrl.replace( /\.git$/, '' ); + + // Remove "/issues" suffix as well. + repositoryUrl = repositoryUrl.replace( /\/issues/, '' ); + + return repositoryUrl; +} -module.exports = transformCommitUtils; diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/truncatechangelog.js b/packages/ckeditor5-dev-release-tools/lib/utils/truncatechangelog.js new file mode 100644 index 000000000..979e2be9d --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/truncatechangelog.js @@ -0,0 +1,42 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { CHANGELOG_HEADER } from './constants.js'; +import { getRepositoryUrl } from './transformcommitutils.js'; +import saveChangelog from './savechangelog.js'; +import getChangelog from './getchangelog.js'; + +/** + * @param {number} length + * @param {string} [cwd=process.cwd()] Where to look for the changelog file. + */ +export default function truncateChangelog( length, cwd = process.cwd() ) { + const changelog = getChangelog( cwd ); + + if ( !changelog ) { + return; + } + + const entryHeader = '## [\\s\\S]+?'; + const entryHeaderRegexp = new RegExp( `\\n(${ entryHeader })(?=\\n${ entryHeader }|$)`, 'g' ); + + const entries = [ ...changelog.matchAll( entryHeaderRegexp ) ] + .filter( match => match && match[ 1 ] ) + .map( match => match[ 1 ] ); + + if ( !entries.length ) { + return; + } + + const truncatedEntries = entries.slice( 0, length ); + + const changelogFooter = entries.length > truncatedEntries.length ? + `\n\n---\n\nTo see all releases, visit the [release page](${ getRepositoryUrl( cwd ) }/releases).\n` : + '\n'; + + const truncatedChangelog = CHANGELOG_HEADER + truncatedEntries.join( '\n' ).trim() + changelogFooter; + + saveChangelog( truncatedChangelog, cwd ); +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/validaterepositorytorelease.js b/packages/ckeditor5-dev-release-tools/lib/utils/validaterepositorytorelease.js index 6a38e894e..6b5afe9f7 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/validaterepositorytorelease.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/validaterepositorytorelease.js @@ -3,19 +3,17 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); +import { tools } from '@ckeditor/ckeditor5-dev-utils'; /** - * @param {Object} options - * @param {String|null} options.version Version of the current release. - * @param {String} options.changes Changelog entries for the current release. - * @param {Boolean} [options.ignoreBranchCheck=false] If set on true, branch checking will be skipped. - * @param {String} [options.branch='master'] A name of the branch that should be used for releasing packages. - * @returns {Promise.>} + * @param {object} options + * @param {string|null} options.version Version of the current release. + * @param {string} options.changes Changelog entries for the current release. + * @param {boolean} [options.ignoreBranchCheck=false] If set on true, branch checking will be skipped. + * @param {string} [options.branch='master'] A name of the branch that should be used for releasing packages. + * @returns {Promise.>} */ -module.exports = async function validateRepositoryToRelease( options ) { +export default async function validateRepositoryToRelease( options ) { const { version, changes, @@ -56,4 +54,4 @@ module.exports = async function validateRepositoryToRelease( options ) { async function exec( command ) { return tools.shExec( command, { verbosity: 'error', async: true } ); } -}; +} diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/versions.js b/packages/ckeditor5-dev-release-tools/lib/utils/versions.js index b7137c717..556fe3de6 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/versions.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/versions.js @@ -3,139 +3,133 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); -const changelogUtils = require( './changelog' ); -const getPackageJson = require( './getpackagejson' ); - -const versions = { - /** - * Returns a last created version in changelog file. - * - * @param {String} [cwd=process.cwd()] Where to look for the changelog file. - * @returns {String|null} - */ - getLastFromChangelog( cwd = process.cwd() ) { - const changelog = changelogUtils.getChangelog( cwd ); - - if ( !changelog ) { - return null; - } - - const regexp = /\n## \[?([\da-z.\-+]+)/i; - const matches = changelog.match( regexp ); - - return matches ? matches[ 1 ] : null; - }, - - /** - * Returns the current (latest) pre-release version that matches the provided release identifier. - * If the package does not have any pre-releases with the provided identifier yet, `null` is returned. - * - * @param {ReleaseIdentifier} releaseIdentifier - * @param {String} [cwd=process.cwd()] - * @returns {Promise.} - */ - getLastPreRelease( releaseIdentifier, cwd = process.cwd() ) { - const packageName = getPackageJson( cwd ).name; - - return tools.shExec( `npm view ${ packageName } versions --json`, { verbosity: 'silent', async: true } ) - .then( result => { - const lastVersion = JSON.parse( result ) - .filter( version => version.startsWith( releaseIdentifier ) ) - .sort( ( a, b ) => a.localeCompare( b, undefined, { numeric: true } ) ) - .pop(); - - return lastVersion || null; - } ) - .catch( () => null ); - }, - - /** - * Returns the current (latest) nightly version in the format of "0.0.0-nightly-YYYYMMDD.X", where the "YYYYMMDD" is the date of the - * last nightly release and the "X" is the sequential number starting from 0. If the package does not have any nightly releases yet, - * `null` is returned. - * - * @param {String} [cwd=process.cwd()] - * @returns {Promise.} - */ - getLastNightly( cwd = process.cwd() ) { - return versions.getLastPreRelease( '0.0.0-nightly', cwd ); - }, - - /** - * Returns the next available pre-release version that matches the following format: ".X", where "X" is the - * next available pre-release sequential number starting from 0. - * - * @param {ReleaseIdentifier} releaseIdentifier - * @param {String} [cwd=process.cwd()] - * @returns {Promise} - */ - async getNextPreRelease( releaseIdentifier, cwd = process.cwd() ) { - const currentPreReleaseVersion = await versions.getLastPreRelease( releaseIdentifier, cwd ); - - if ( !currentPreReleaseVersion ) { - return `${ releaseIdentifier }.0`; - } - - const currentPreReleaseVersionTokens = currentPreReleaseVersion.split( '.' ); - const currentPreReleaseSequenceNumber = currentPreReleaseVersionTokens.pop(); - const currentPreReleaseIdentifier = currentPreReleaseVersionTokens.join( '.' ); - const nextPreReleaseSequenceNumber = Number( currentPreReleaseSequenceNumber ) + 1; - - return `${ currentPreReleaseIdentifier }.${ nextPreReleaseSequenceNumber }`; - }, - - /** - * Returns the next available nightly version in the format of "0.0.0-nightly-YYYYMMDD.X", where the "YYYYMMDD" is the current date for - * the nightly release and the "X" is the sequential number starting from 0. - * - * @param {String} [cwd=process.cwd()] - * @returns {Promise} - */ - async getNextNightly( cwd = process.cwd() ) { - const today = new Date(); - const year = today.getFullYear().toString(); - const month = ( today.getMonth() + 1 ).toString().padStart( 2, '0' ); - const day = today.getDate().toString().padStart( 2, '0' ); - - const nextNightlyReleaseIdentifier = `0.0.0-nightly-${ year }${ month }${ day }`; - - return versions.getNextPreRelease( nextNightlyReleaseIdentifier, cwd ); - }, - - /** - * Returns a name of the last created tag. - * - * @returns {String|null} - */ - getLastTagFromGit() { - try { - const lastTag = tools.shExec( 'git describe --abbrev=0 --tags 2> /dev/null', { verbosity: 'error' } ); - - return lastTag.trim().replace( /^v/, '' ) || null; - } catch ( err ) { - /* istanbul ignore next */ - return null; - } - }, - - /** - * Returns version of current package from `package.json`. - * - * @param {String} [cwd=process.cwd()] Current work directory. - * @returns {String} - */ - getCurrent( cwd = process.cwd() ) { - return getPackageJson( cwd ).version; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import getChangelog from './getchangelog.js'; +import getPackageJson from './getpackagejson.js'; + +/** + * Returns a last created version in changelog file. + * + * @param {string} [cwd=process.cwd()] Where to look for the changelog file. + * @returns {string|null} + */ +export function getLastFromChangelog( cwd = process.cwd() ) { + const changelog = getChangelog( cwd ); + + if ( !changelog ) { + return null; } -}; -module.exports = versions; + const regexp = /\n## \[?([\da-z.\-+]+)/i; + const matches = changelog.match( regexp ); + + return matches ? matches[ 1 ] : null; +} + +/** + * Returns the current (latest) pre-release version that matches the provided release identifier. + * If the package does not have any pre-releases with the provided identifier yet, `null` is returned. + * + * @param {ReleaseIdentifier} releaseIdentifier + * @param {string} [cwd=process.cwd()] + * @returns {Promise.} + */ +export function getLastPreRelease( releaseIdentifier, cwd = process.cwd() ) { + const packageName = getPackageJson( cwd ).name; + + return tools.shExec( `npm view ${ packageName } versions --json`, { verbosity: 'silent', async: true } ) + .then( result => { + const lastVersion = JSON.parse( result ) + .filter( version => version.startsWith( releaseIdentifier ) ) + .sort( ( a, b ) => a.localeCompare( b, undefined, { numeric: true } ) ) + .pop(); + + return lastVersion || null; + } ) + .catch( () => null ); +} + +/** + * Returns the current (latest) nightly version in the format of "0.0.0-nightly-YYYYMMDD.X", where the "YYYYMMDD" is the date of the + * last nightly release and the "X" is the sequential number starting from 0. If the package does not have any nightly releases yet, + * `null` is returned. + * + * @param {string} [cwd=process.cwd()] + * @returns {Promise.} + */ +export function getLastNightly( cwd = process.cwd() ) { + return getLastPreRelease( '0.0.0-nightly', cwd ); +} + +/** + * Returns the next available pre-release version that matches the following format: ".X", where "X" is the + * next available pre-release sequential number starting from 0. + * + * @param {ReleaseIdentifier} releaseIdentifier + * @param {string} [cwd=process.cwd()] + * @returns {Promise} + */ +export async function getNextPreRelease( releaseIdentifier, cwd = process.cwd() ) { + const currentPreReleaseVersion = await getLastPreRelease( releaseIdentifier, cwd ); + + if ( !currentPreReleaseVersion ) { + return `${ releaseIdentifier }.0`; + } + + const currentPreReleaseVersionTokens = currentPreReleaseVersion.split( '.' ); + const currentPreReleaseSequenceNumber = currentPreReleaseVersionTokens.pop(); + const currentPreReleaseIdentifier = currentPreReleaseVersionTokens.join( '.' ); + const nextPreReleaseSequenceNumber = Number( currentPreReleaseSequenceNumber ) + 1; + + return `${ currentPreReleaseIdentifier }.${ nextPreReleaseSequenceNumber }`; +} + +/** + * Returns the next available nightly version in the format of "0.0.0-nightly-YYYYMMDD.X", where the "YYYYMMDD" is the current date for + * the nightly release and the "X" is the sequential number starting from 0. + * + * @param {string} [cwd=process.cwd()] + * @returns {Promise} + */ +export async function getNextNightly( cwd = process.cwd() ) { + const today = new Date(); + const year = today.getFullYear().toString(); + const month = ( today.getMonth() + 1 ).toString().padStart( 2, '0' ); + const day = today.getDate().toString().padStart( 2, '0' ); + + const nextNightlyReleaseIdentifier = `0.0.0-nightly-${ year }${ month }${ day }`; + + return getNextPreRelease( nextNightlyReleaseIdentifier, cwd ); +} + +/** + * Returns a name of the last created tag. + * + * @returns {string|null} + */ +export function getLastTagFromGit() { + try { + const lastTag = tools.shExec( 'git describe --abbrev=0 --tags 2> /dev/null', { verbosity: 'error' } ); + + return lastTag.trim().replace( /^v/, '' ) || null; + } catch ( err ) { + /* istanbul ignore next */ + return null; + } +} + +/** + * Returns version of current package from `package.json`. + * + * @param {string} [cwd=process.cwd()] Current work directory. + * @returns {string} + */ +export function getCurrent( cwd = process.cwd() ) { + return getPackageJson( cwd ).version; +} /** - * @typedef {String} ReleaseIdentifier The pre-release identifier without the last dynamic part (the pre-release sequential number). + * @typedef {string} ReleaseIdentifier The pre-release identifier without the last dynamic part (the pre-release sequential number). * It consists of the core base version (".."), a hyphen ("-"), and a pre-release identifier name (e.g. "alpha"). * * Examples: diff --git a/packages/ckeditor5-dev-release-tools/package.json b/packages/ckeditor5-dev-release-tools/package.json index 46016d4e6..6f691b956 100644 --- a/packages/ckeditor5-dev-release-tools/package.json +++ b/packages/ckeditor5-dev-release-tools/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-release-tools", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "Tools used for releasing CKEditor 5 and related packages.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -16,42 +16,40 @@ "node": ">=18.0.0", "npm": ">=5.7.1" }, + "type": "module", "main": "lib/index.js", "files": [ "lib" ], "dependencies": { - "@ckeditor/ckeditor5-dev-utils": "^43.0.0", - "@octokit/rest": "^19.0.0", - "chalk": "^4.0.0", + "@ckeditor/ckeditor5-dev-utils": "^44.0.0-alpha.5", + "@octokit/rest": "^21.0.0", + "chalk": "^5.0.0", "cli-columns": "^4.0.0", "compare-func": "^2.0.0", "concat-stream": "^2.0.0", - "conventional-changelog-writer": "^6.0.0", - "conventional-commits-filter": "^3.0.0", - "conventional-commits-parser": "^4.0.0", - "date-fns": "^2.30.0", - "fs-extra": "^11.2.0", - "git-raw-commits": "^3.0.0", - "glob": "^10.2.5", - "inquirer": "^7.1.0", - "lodash": "^4.17.15", - "minimatch": "^3.0.4", - "semver": "^7.5.3", + "conventional-changelog-writer": "^8.0.0", + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.0.0", + "date-fns": "^4.0.0", + "fs-extra": "^11.0.0", + "git-raw-commits": "^5.0.0", + "glob": "^10.0.0", + "inquirer": "^11.0.0", + "lodash-es": "^4.17.21", + "minimatch": "^9.0.0", + "semver": "^7.6.3", "shell-escape": "^0.2.0", "upath": "^2.0.1" }, "devDependencies": { - "chai": "^4.2.0", + "jest-extended": "^4.0.2", + "vitest": "^2.0.5", "handlebars": "^4.7.6", - "mocha": "^7.1.2", - "mockery": "^2.1.0", - "mock-fs": "^5.1.2", - "proxyquire": "^2.1.3", - "sinon": "^9.2.4" + "mock-fs": "^5.2.0" }, "scripts": { - "test": "mocha './tests/**/*.js' --timeout 10000", - "coverage": "nyc --reporter=lcov --reporter=text-summary yarn run test" + "test": "vitest run --config vitest.config.js", + "coverage": "vitest run --config vitest.config.js --coverage" } } diff --git a/packages/jsdoc-plugins/tests/integration-tests/_utils/logger.js b/packages/ckeditor5-dev-release-tools/tests/_utils/testsetup.js similarity index 53% rename from packages/jsdoc-plugins/tests/integration-tests/_utils/logger.js rename to packages/ckeditor5-dev-release-tools/tests/_utils/testsetup.js index 8a852b3bb..52e540b96 100644 --- a/packages/jsdoc-plugins/tests/integration-tests/_utils/logger.js +++ b/packages/ckeditor5-dev-release-tools/tests/_utils/testsetup.js @@ -3,10 +3,7 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { expect } from 'vitest'; +import * as matchers from 'jest-extended'; -exports.handlers = { - processingComplete( e ) { - console.log( JSON.stringify( e, null, 4 ) ); - } -}; +expect.extend( matchers ); diff --git a/packages/ckeditor5-dev-release-tools/tests/index.js b/packages/ckeditor5-dev-release-tools/tests/index.js index 9bfc2b503..b483a9b17 100644 --- a/packages/ckeditor5-dev-release-tools/tests/index.js +++ b/packages/ckeditor5-dev-release-tools/tests/index.js @@ -3,249 +3,253 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const sinon = require( 'sinon' ); -const expect = require( 'chai' ).expect; -const proxyquire = require( 'proxyquire' ); -const mockery = require( 'mockery' ); +import { describe, expect, it, vi } from 'vitest'; +import generateChangelogForSinglePackage from '../lib/tasks/generatechangelogforsinglepackage.js'; +import generateChangelogForMonoRepository from '../lib/tasks/generatechangelogformonorepository.js'; +import updateDependencies from '../lib/tasks/updatedependencies.js'; +import commitAndTag from '../lib/tasks/commitandtag.js'; +import createGithubRelease from '../lib/tasks/creategithubrelease.js'; +import reassignNpmTags from '../lib/tasks/reassignnpmtags.js'; +import prepareRepository from '../lib/tasks/preparerepository.js'; +import push from '../lib/tasks/push.js'; +import publishPackages from '../lib/tasks/publishpackages.js'; +import updateVersions from '../lib/tasks/updateversions.js'; +import cleanUpPackages from '../lib/tasks/cleanuppackages.js'; +import getChangesForVersion from '../lib/utils/getchangesforversion.js'; +import getChangelog from '../lib/utils/getchangelog.js'; +import saveChangelog from '../lib/utils/savechangelog.js'; +import { + getLastFromChangelog, + getLastPreRelease, + getNextPreRelease, + getLastNightly, + getNextNightly, + getCurrent, + getLastTagFromGit +} from '../lib/utils/versions.js'; +import executeInParallel from '../lib/utils/executeinparallel.js'; +import validateRepositoryToRelease from '../lib/utils/validaterepositorytorelease.js'; +import checkVersionAvailability from '../lib/utils/checkversionavailability.js'; +import verifyPackagesPublishedCorrectly from '../lib/tasks/verifypackagespublishedcorrectly.js'; +import getNpmTagFromVersion from '../lib/utils/getnpmtagfromversion.js'; +import isVersionPublishableForTag from '../lib/utils/isversionpublishablefortag.js'; +import provideToken from '../lib/utils/providetoken.js'; + +import * as index from '../lib/index.js'; + +vi.mock( '../lib/tasks/generatechangelogforsinglepackage' ); +vi.mock( '../lib/tasks/generatechangelogformonorepository' ); +vi.mock( '../lib/tasks/updatedependencies' ); +vi.mock( '../lib/tasks/commitandtag' ); +vi.mock( '../lib/tasks/creategithubrelease' ); +vi.mock( '../lib/tasks/reassignnpmtags' ); +vi.mock( '../lib/tasks/preparerepository' ); +vi.mock( '../lib/tasks/push' ); +vi.mock( '../lib/tasks/publishpackages' ); +vi.mock( '../lib/tasks/updateversions' ); +vi.mock( '../lib/tasks/cleanuppackages' ); +vi.mock( '../lib/utils/versions' ); +vi.mock( '../lib/utils/getnpmtagfromversion' ); +vi.mock( '../lib/utils/changelog' ); +vi.mock( '../lib/utils/executeinparallel' ); +vi.mock( '../lib/utils/validaterepositorytorelease' ); +vi.mock( '../lib/utils/isversionpublishablefortag' ); +vi.mock( '../lib/utils/provideToken' ); describe( 'dev-release-tools/index', () => { - let index, sandbox, stubs; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - logger: { - info: sandbox.spy(), - warning: sandbox.spy(), - error: sandbox.spy() - }, - release: { - generateChangelogForSinglePackage: sandbox.stub(), - generateChangelogForMonoRepository: sandbox.stub(), - updateDependencies: sandbox.stub(), - commitAndTag: sandbox.stub(), - createGithubRelease: sandbox.stub(), - reassignNpmTags: sandbox.stub(), - prepareRepository: sandbox.stub(), - push: sandbox.stub(), - publishPackages: sandbox.stub(), - updateVersions: sandbox.stub(), - cleanUpPackages: sandbox.stub(), - version: { - getLastFromChangelog: sandbox.stub(), - getLastPreRelease: sandbox.stub(), - getNextPreRelease: sandbox.stub(), - getLastNightly: sandbox.stub(), - getNextNightly: sandbox.stub(), - getCurrent: sandbox.stub(), - getLastTagFromGit: sandbox.stub() - }, - changelog: { - getChangesForVersion: sandbox.stub(), - getChangelog: sandbox.stub(), - saveChangelog: sandbox.stub() - }, - executeInParallel: sandbox.stub(), - validateRepositoryToRelease: sandbox.stub(), - getNpmTagFromVersion: sandbox.stub(), - isVersionPublishableForTag: sandbox.stub() - } - }; - - mockery.registerMock( './tasks/generatechangelogforsinglepackage', stubs.release.generateChangelogForSinglePackage ); - mockery.registerMock( './tasks/generatechangelogformonorepository', stubs.release.generateChangelogForMonoRepository ); - mockery.registerMock( './tasks/updatedependencies', stubs.release.updateDependencies ); - mockery.registerMock( './tasks/commitandtag', stubs.release.commitAndTag ); - mockery.registerMock( './tasks/creategithubrelease', stubs.release.createGithubRelease ); - mockery.registerMock( './tasks/reassignnpmtags', stubs.release.reassignNpmTags ); - mockery.registerMock( './tasks/preparerepository', stubs.release.prepareRepository ); - mockery.registerMock( './tasks/push', stubs.release.push ); - mockery.registerMock( './tasks/publishpackages', stubs.release.publishPackages ); - mockery.registerMock( './tasks/updateversions', stubs.release.updateVersions ); - mockery.registerMock( './tasks/cleanuppackages', stubs.release.cleanUpPackages ); - mockery.registerMock( './utils/versions', stubs.release.version ); - mockery.registerMock( './utils/getnpmtagfromversion', stubs.release.getNpmTagFromVersion ); - mockery.registerMock( './utils/changelog', stubs.release.changelog ); - mockery.registerMock( './utils/executeinparallel', stubs.release.executeInParallel ); - mockery.registerMock( './utils/validaterepositorytorelease', stubs.release.validateRepositoryToRelease ); - mockery.registerMock( './utils/isversionpublishablefortag', stubs.release.isVersionPublishableForTag ); - - index = proxyquire( '../lib/index', { - '@ckeditor/ckeditor5-dev-utils': { - logger() { - return stubs.logger; - } - } - } ); - } ); - - afterEach( () => { - sandbox.restore(); - mockery.disable(); - } ); - describe( 'generateChangelogForSinglePackage()', () => { it( 'should be a function', () => { expect( index.generateChangelogForSinglePackage ).to.be.a( 'function' ); + expect( index.generateChangelogForSinglePackage ).to.equal( generateChangelogForSinglePackage ); } ); } ); describe( 'generateChangelogForMonoRepository()', () => { it( 'should be a function', () => { - expect( index.generateChangelogForMonoRepository ).to.be.a( 'function' ); + expect( generateChangelogForMonoRepository ).to.be.a( 'function' ); + expect( index.generateChangelogForMonoRepository ).to.equal( generateChangelogForMonoRepository ); } ); } ); describe( 'updateDependencies()', () => { it( 'should be a function', () => { - expect( index.updateDependencies ).to.be.a( 'function' ); + expect( updateDependencies ).to.be.a( 'function' ); + expect( index.updateDependencies ).to.equal( updateDependencies ); } ); } ); describe( 'commitAndTag()', () => { it( 'should be a function', () => { - expect( index.commitAndTag ).to.be.a( 'function' ); + expect( commitAndTag ).to.be.a( 'function' ); + expect( index.commitAndTag ).to.equal( commitAndTag ); } ); } ); describe( 'createGithubRelease()', () => { it( 'should be a function', () => { - expect( index.createGithubRelease ).to.be.a( 'function' ); + expect( createGithubRelease ).to.be.a( 'function' ); + expect( index.createGithubRelease ).to.equal( createGithubRelease ); } ); } ); describe( 'reassignNpmTags()', () => { it( 'should be a function', () => { - expect( index.reassignNpmTags ).to.be.a( 'function' ); + expect( reassignNpmTags ).to.be.a( 'function' ); + expect( index.reassignNpmTags ).to.equal( reassignNpmTags ); } ); } ); describe( 'prepareRepository()', () => { it( 'should be a function', () => { - expect( index.prepareRepository ).to.be.a( 'function' ); + expect( prepareRepository ).to.be.a( 'function' ); + expect( index.prepareRepository ).to.equal( prepareRepository ); } ); } ); describe( 'push()', () => { it( 'should be a function', () => { - expect( index.push ).to.be.a( 'function' ); + expect( push ).to.be.a( 'function' ); + expect( index.push ).to.equal( push ); } ); } ); describe( 'publishPackages()', () => { it( 'should be a function', () => { - expect( index.publishPackages ).to.be.a( 'function' ); + expect( publishPackages ).to.be.a( 'function' ); + expect( index.publishPackages ).to.equal( publishPackages ); } ); } ); describe( 'updateVersions()', () => { it( 'should be a function', () => { - expect( index.updateVersions ).to.be.a( 'function' ); + expect( updateVersions ).to.be.a( 'function' ); + expect( index.updateVersions ).to.equal( updateVersions ); } ); } ); describe( 'cleanUpPackages()', () => { it( 'should be a function', () => { - expect( index.cleanUpPackages ).to.be.a( 'function' ); + expect( cleanUpPackages ).to.be.a( 'function' ); + expect( index.cleanUpPackages ).to.equal( cleanUpPackages ); } ); } ); describe( 'getLastFromChangelog()', () => { it( 'should be a function', () => { - expect( index.getLastFromChangelog ).to.be.a( 'function' ); + expect( getLastFromChangelog ).to.be.a( 'function' ); + expect( index.getLastFromChangelog ).to.equal( getLastFromChangelog ); } ); } ); describe( 'getCurrent()', () => { it( 'should be a function', () => { - expect( index.getCurrent ).to.be.a( 'function' ); + expect( getCurrent ).to.be.a( 'function' ); + expect( index.getCurrent ).to.equal( getCurrent ); } ); } ); describe( 'getLastPreRelease()', () => { it( 'should be a function', () => { - expect( index.getLastPreRelease ).to.be.a( 'function' ); + expect( getLastPreRelease ).to.be.a( 'function' ); + expect( index.getLastPreRelease ).to.equal( getLastPreRelease ); } ); } ); describe( 'getNextPreRelease()', () => { it( 'should be a function', () => { - expect( index.getNextPreRelease ).to.be.a( 'function' ); + expect( getNextPreRelease ).to.be.a( 'function' ); + expect( index.getNextPreRelease ).to.equal( getNextPreRelease ); } ); } ); describe( 'getLastNightly()', () => { it( 'should be a function', () => { - expect( index.getLastNightly ).to.be.a( 'function' ); + expect( getLastNightly ).to.be.a( 'function' ); + expect( index.getLastNightly ).to.equal( getLastNightly ); } ); } ); describe( 'getNextNightly()', () => { it( 'should be a function', () => { - expect( index.getNextNightly ).to.be.a( 'function' ); + expect( getNextNightly ).to.be.a( 'function' ); + expect( index.getNextNightly ).to.equal( getNextNightly ); } ); } ); describe( 'getLastTagFromGit()', () => { it( 'should be a function', () => { - expect( index.getLastTagFromGit ).to.be.a( 'function' ); + expect( getLastTagFromGit ).to.be.a( 'function' ); + expect( index.getLastTagFromGit ).to.equal( getLastTagFromGit ); } ); } ); describe( 'getNpmTagFromVersion()', () => { it( 'should be a function', () => { - expect( index.getNpmTagFromVersion ).to.be.a( 'function' ); + expect( getNpmTagFromVersion ).to.be.a( 'function' ); + expect( index.getNpmTagFromVersion ).to.equal( getNpmTagFromVersion ); } ); } ); describe( 'getChangesForVersion()', () => { it( 'should be a function', () => { - expect( index.getChangesForVersion ).to.be.a( 'function' ); + expect( getChangesForVersion ).to.be.a( 'function' ); + expect( index.getChangesForVersion ).to.equal( getChangesForVersion ); } ); } ); describe( 'getChangelog()', () => { it( 'should be a function', () => { - expect( index.getChangelog ).to.be.a( 'function' ); + expect( getChangelog ).to.be.a( 'function' ); + expect( index.getChangelog ).to.equal( getChangelog ); } ); } ); describe( 'saveChangelog()', () => { it( 'should be a function', () => { - expect( index.saveChangelog ).to.be.a( 'function' ); + expect( saveChangelog ).to.be.a( 'function' ); + expect( index.saveChangelog ).to.equal( saveChangelog ); } ); } ); describe( 'executeInParallel()', () => { it( 'should be a function', () => { - expect( index.executeInParallel ).to.be.a( 'function' ); + expect( executeInParallel ).to.be.a( 'function' ); + expect( index.executeInParallel ).to.equal( executeInParallel ); } ); } ); describe( 'validateRepositoryToRelease()', () => { it( 'should be a function', () => { - expect( index.validateRepositoryToRelease ).to.be.a( 'function' ); + expect( validateRepositoryToRelease ).to.be.a( 'function' ); + expect( index.validateRepositoryToRelease ).to.equal( validateRepositoryToRelease ); } ); } ); describe( 'checkVersionAvailability()', () => { it( 'should be a function', () => { - expect( index.checkVersionAvailability ).to.be.a( 'function' ); + expect( checkVersionAvailability ).to.be.a( 'function' ); + expect( index.checkVersionAvailability ).to.equal( checkVersionAvailability ); } ); } ); describe( 'isVersionPublishableForTag()', () => { it( 'should be a function', () => { - expect( index.isVersionPublishableForTag ).to.be.a( 'function' ); + expect( isVersionPublishableForTag ).to.be.a( 'function' ); + expect( index.isVersionPublishableForTag ).to.equal( isVersionPublishableForTag ); + } ); + } ); + + describe( 'verifyPackagesPublishedCorrectly()', () => { + it( 'should be a function', () => { + expect( verifyPackagesPublishedCorrectly ).to.be.a( 'function' ); + expect( index.verifyPackagesPublishedCorrectly ).to.equal( verifyPackagesPublishedCorrectly ); + } ); + } ); + + describe( 'provideToken()', () => { + it( 'should be a function', () => { + expect( provideToken ).to.be.a( 'function' ); + expect( index.provideToken ).to.equal( provideToken ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/cleanuppackages.js b/packages/ckeditor5-dev-release-tools/tests/tasks/cleanuppackages.js index 5f925b337..c9ebd4f71 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/cleanuppackages.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/cleanuppackages.js @@ -3,761 +3,750 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); -const fs = require( 'fs-extra' ); -const upath = require( 'upath' ); -const { glob } = require( 'glob' ); - -const mockFs = require( 'mock-fs' ); +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import fs from 'fs-extra'; +import upath from 'upath'; +import { glob } from 'glob'; +import mockFs from 'mock-fs'; + +describe( 'cleanUpPackages()', () => { + let cleanUpPackages, stubs; + + beforeEach( async () => { + // Calls to `fs` and `glob` are stubbed, but they are passed through to the real implementation, because we want to test the + // real behavior of the script. The whole filesystem is mocked by the `mock-fs` util for testing purposes. A virtual project is + // prepared in tests on this mocked filesystem. + vi.doMock( 'glob', () => ( { + glob: vi.fn().mockImplementation( glob ) + } ) ); + vi.doMock( 'fs-extra', () => ( { + default: { + readJson: vi.fn().mockImplementation( fs.readJson ), + writeJson: vi.fn().mockImplementation( fs.writeJson ), + remove: vi.fn().mockImplementation( fs.remove ), + readdir: vi.fn().mockImplementation( fs.readdir ) + } + } ) ); + + stubs = { + ...await import( 'glob' ), + ...( await import( 'fs-extra' ) ).default + }; + + cleanUpPackages = ( await import( '../../lib/tasks/cleanuppackages.js' ) ).default; + } ); -describe( 'dev-release-tools/tasks', () => { - describe( 'cleanUpPackages()', () => { - let cleanUpPackages, sandbox, stubs; + afterEach( () => { + vi.resetModules(); + mockFs.restore(); + } ); + describe( 'preparing options', () => { beforeEach( () => { - sandbox = sinon.createSandbox(); - - // Calls to `fs` and `glob` are stubbed, but they are passed through to the real implementation, because we want to test the - // real behavior of the script. The whole filesystem is mocked by the `mock-fs` util for testing purposes. A virtual project is - // prepared in tests on this mocked filesystem. - stubs = { - fs: { - readJson: sandbox.stub().callsFake( ( ...args ) => fs.readJson( ...args ) ), - writeJson: sandbox.stub().callsFake( ( ...args ) => fs.writeJson( ...args ) ), - remove: sandbox.stub().callsFake( ( ...args ) => fs.remove( ...args ) ), - readdir: sandbox.stub().callsFake( ( ...args ) => fs.readdir( ...args ) ) - }, - glob: { - glob: sandbox.stub().callsFake( ( ...args ) => glob( ...args ) ) - } - }; + mockFs( {} ); + } ); - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false + it( 'should use provided `cwd` to search for packages', async () => { + await cleanUpPackages( { + packagesDirectory: 'release', + cwd: '/work/another/project' } ); - mockery.registerMock( 'fs-extra', stubs.fs ); - mockery.registerMock( 'glob', stubs.glob ); - - cleanUpPackages = require( '../../lib/tasks/cleanuppackages' ); + expect( stubs.glob ).toHaveBeenCalledExactlyOnceWith( expect.any( String ), expect.objectContaining( { + cwd: '/work/another/project/release' + } ) ); } ); - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - mockFs.restore(); - sandbox.restore(); - } ); + it( 'should use `process.cwd()` to search for packages if `cwd` option is not provided', async () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/work/project' ); - describe( 'preparing options', () => { - beforeEach( () => { - mockFs( {} ); + await cleanUpPackages( { + packagesDirectory: 'release' } ); - it( 'should use provided `cwd` to search for packages', async () => { - await cleanUpPackages( { - packagesDirectory: 'release', - cwd: '/work/another/project' - } ); + expect( stubs.glob ).toHaveBeenCalledExactlyOnceWith( expect.any( String ), expect.objectContaining( { + cwd: '/work/project/release' + } ) ); + } ); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 1 ] ).to.have.property( 'cwd', '/work/another/project/release' ); + it( 'should match only files', async () => { + await cleanUpPackages( { + packagesDirectory: 'release' } ); - it( 'should use `process.cwd()` to search for packages if `cwd` option is not provided', async () => { - sandbox.stub( process, 'cwd' ).returns( '/work/project' ); - - await cleanUpPackages( { - packagesDirectory: 'release' - } ); + expect( stubs.glob ).toHaveBeenCalledExactlyOnceWith( expect.any( String ), expect.objectContaining( { + nodir: true + } ) ); + } ); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 1 ] ).to.have.property( 'cwd', '/work/project/release' ); + it( 'should always receive absolute paths for matched files', async () => { + await cleanUpPackages( { + packagesDirectory: 'release' } ); - it( 'should match only files', async () => { - await cleanUpPackages( { - packagesDirectory: 'release' - } ); + expect( stubs.glob ).toHaveBeenCalledExactlyOnceWith( expect.any( String ), expect.objectContaining( { + absolute: true + } ) ); + } ); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 1 ] ).to.have.property( 'nodir', true ); + it( 'should search for `package.json` in `cwd`', async () => { + await cleanUpPackages( { + packagesDirectory: 'release' } ); - it( 'should always receive absolute paths for matched files', async () => { - await cleanUpPackages( { - packagesDirectory: 'release' - } ); + expect( stubs.glob ).toHaveBeenCalledExactlyOnceWith( '*/package.json', expect.any( Object ) ); + } ); + } ); + + describe( 'cleaning package directory', () => { + it( 'should remove empty directories', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo' + } ), + 'ckeditor5-metadata.json': '', + 'src': {} + } + } + } ); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 1 ] ).to.have.property( 'absolute', true ); + await cleanUpPackages( { + packagesDirectory: 'release' } ); - it( 'should search for `package.json` in `cwd`', async () => { - await cleanUpPackages( { - packagesDirectory: 'release' - } ); + const actualPaths = await getAllPaths(); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 0 ] ).to.equal( '*/package.json' ); - } ); + expect( actualPaths ).to.have.members( [ + getPathTo( '.' ), + getPathTo( 'release' ), + getPathTo( 'release/ckeditor5-foo' ), + getPathTo( 'release/ckeditor5-foo/package.json' ), + getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ) + ] ); } ); - describe( 'cleaning package directory', () => { - it( 'should remove empty directories', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo' - } ), - 'ckeditor5-metadata.json': '', - 'src': {} - } - } - } ); - - await cleanUpPackages( { - packagesDirectory: 'release' - } ); - - const actualPaths = await getAllPaths(); - - expect( actualPaths ).to.have.members( [ - getPathTo( '.' ), - getPathTo( 'release' ), - getPathTo( 'release/ckeditor5-foo' ), - getPathTo( 'release/ckeditor5-foo/package.json' ), - getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ) - ] ); - } ); - - it( 'should remove `node_modules`', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo' - } ), - 'ckeditor5-metadata.json': '', - 'node_modules': { - '.bin': {}, - '@ckeditor': { - 'ckeditor5-dev-release-tools': { - 'package.json': '' - } + it( 'should remove `node_modules`', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo' + } ), + 'ckeditor5-metadata.json': '', + 'node_modules': { + '.bin': {}, + '@ckeditor': { + 'ckeditor5-dev-release-tools': { + 'package.json': '' } } } } - } ); - - await cleanUpPackages( { - packagesDirectory: 'release' - } ); - - const actualPaths = await getAllPaths(); - - expect( actualPaths ).to.have.members( [ - getPathTo( '.' ), - getPathTo( 'release' ), - getPathTo( 'release/ckeditor5-foo' ), - getPathTo( 'release/ckeditor5-foo/package.json' ), - getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ) - ] ); - } ); - - it( 'should not remove any file if `files` field is not set', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo' - } ), - 'ckeditor5-metadata.json': '' - } + } + } ); + + await cleanUpPackages( { + packagesDirectory: 'release' + } ); + + const actualPaths = await getAllPaths(); + + expect( actualPaths ).to.have.members( [ + getPathTo( '.' ), + getPathTo( 'release' ), + getPathTo( 'release/ckeditor5-foo' ), + getPathTo( 'release/ckeditor5-foo/package.json' ), + getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ) + ] ); + } ); + + it( 'should not remove any file if `files` field is not set', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo' + } ), + 'ckeditor5-metadata.json': '' } - } ); - - await cleanUpPackages( { - packagesDirectory: 'release' - } ); - - const actualPaths = await getAllPaths(); - - expect( actualPaths ).to.have.members( [ - getPathTo( '.' ), - getPathTo( 'release' ), - getPathTo( 'release/ckeditor5-foo' ), - getPathTo( 'release/ckeditor5-foo/package.json' ), - getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ) - ] ); - } ); - - it( 'should not remove mandatory files', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo', - main: 'src/index.js', - types: 'src/index.d.ts', - files: [ - 'foo' - ] - } ), - 'README.md': '', - 'LICENSE.md': '', - 'src': { - 'index.js': '', - 'index.d.ts': '' - } + } + } ); + + await cleanUpPackages( { + packagesDirectory: 'release' + } ); + + const actualPaths = await getAllPaths(); + + expect( actualPaths ).to.have.members( [ + getPathTo( '.' ), + getPathTo( 'release' ), + getPathTo( 'release/ckeditor5-foo' ), + getPathTo( 'release/ckeditor5-foo/package.json' ), + getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ) + ] ); + } ); + + it( 'should not remove mandatory files', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo', + main: 'src/index.js', + types: 'src/index.d.ts', + files: [ + 'foo' + ] + } ), + 'README.md': '', + 'LICENSE.md': '', + 'src': { + 'index.js': '', + 'index.d.ts': '' } } - } ); - - await cleanUpPackages( { - packagesDirectory: 'release' - } ); - - const actualPaths = await getAllPaths(); - - expect( actualPaths ).to.have.members( [ - getPathTo( '.' ), - getPathTo( 'release' ), - getPathTo( 'release/ckeditor5-foo' ), - getPathTo( 'release/ckeditor5-foo/package.json' ), - getPathTo( 'release/ckeditor5-foo/README.md' ), - getPathTo( 'release/ckeditor5-foo/LICENSE.md' ), - getPathTo( 'release/ckeditor5-foo/src' ), - getPathTo( 'release/ckeditor5-foo/src/index.js' ), - getPathTo( 'release/ckeditor5-foo/src/index.d.ts' ) - ] ); - } ); - - it( 'should remove not matched dot files and dot directories', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - '.github': { - 'template.md': '' - }, - '.eslintrc.js': '', - '.IMPORTANT.md': '', - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo', - files: [ - '.IMPORTANT.md', - 'src' - ] - } ), - 'README.md': '', - 'LICENSE.md': '', - 'src': { - 'index.js': '', - 'index.d.ts': '' - } + } + } ); + + await cleanUpPackages( { + packagesDirectory: 'release' + } ); + + const actualPaths = await getAllPaths(); + + expect( actualPaths ).to.have.members( [ + getPathTo( '.' ), + getPathTo( 'release' ), + getPathTo( 'release/ckeditor5-foo' ), + getPathTo( 'release/ckeditor5-foo/package.json' ), + getPathTo( 'release/ckeditor5-foo/README.md' ), + getPathTo( 'release/ckeditor5-foo/LICENSE.md' ), + getPathTo( 'release/ckeditor5-foo/src' ), + getPathTo( 'release/ckeditor5-foo/src/index.js' ), + getPathTo( 'release/ckeditor5-foo/src/index.d.ts' ) + ] ); + } ); + + it( 'should remove not matched dot files and dot directories', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + '.github': { + 'template.md': '' + }, + '.eslintrc.js': '', + '.IMPORTANT.md': '', + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo', + files: [ + '.IMPORTANT.md', + 'src' + ] + } ), + 'README.md': '', + 'LICENSE.md': '', + 'src': { + 'index.js': '', + 'index.d.ts': '' } } - } ); - - await cleanUpPackages( { - packagesDirectory: 'release' - } ); - - const actualPaths = await getAllPaths(); - - expect( actualPaths ).to.have.members( [ - getPathTo( '.' ), - getPathTo( 'release' ), - getPathTo( 'release/ckeditor5-foo' ), - getPathTo( 'release/ckeditor5-foo/.IMPORTANT.md' ), - getPathTo( 'release/ckeditor5-foo/package.json' ), - getPathTo( 'release/ckeditor5-foo/README.md' ), - getPathTo( 'release/ckeditor5-foo/LICENSE.md' ), - getPathTo( 'release/ckeditor5-foo/src' ), - getPathTo( 'release/ckeditor5-foo/src/index.js' ), - getPathTo( 'release/ckeditor5-foo/src/index.d.ts' ) - ] ); - } ); - - it( 'should remove not matched files, empty directories and `node_modules` - pattern without globs', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo', - files: [ - 'ckeditor5-metadata.json', - 'src' - ] - } ), - 'README.md': '', - 'LICENSE.md': '', - 'ckeditor5-metadata.json': '', - 'docs': { - 'assets': { - 'img': { - 'asset.png': '' - } - }, - 'api': { - 'foo.md': '' - }, - 'features': { - 'foo.md': '' + } + } ); + + await cleanUpPackages( { + packagesDirectory: 'release' + } ); + + const actualPaths = await getAllPaths(); + + expect( actualPaths ).to.have.members( [ + getPathTo( '.' ), + getPathTo( 'release' ), + getPathTo( 'release/ckeditor5-foo' ), + getPathTo( 'release/ckeditor5-foo/.IMPORTANT.md' ), + getPathTo( 'release/ckeditor5-foo/package.json' ), + getPathTo( 'release/ckeditor5-foo/README.md' ), + getPathTo( 'release/ckeditor5-foo/LICENSE.md' ), + getPathTo( 'release/ckeditor5-foo/src' ), + getPathTo( 'release/ckeditor5-foo/src/index.js' ), + getPathTo( 'release/ckeditor5-foo/src/index.d.ts' ) + ] ); + } ); + + it( 'should remove not matched files, empty directories and `node_modules` - pattern without globs', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo', + files: [ + 'ckeditor5-metadata.json', + 'src' + ] + } ), + 'README.md': '', + 'LICENSE.md': '', + 'ckeditor5-metadata.json': '', + 'docs': { + 'assets': { + 'img': { + 'asset.png': '' } }, - 'node_modules': { - '.bin': {}, - '@ckeditor': { - 'ckeditor5-dev-release-tools': { - 'package.json': '' - } + 'api': { + 'foo.md': '' + }, + 'features': { + 'foo.md': '' + } + }, + 'node_modules': { + '.bin': {}, + '@ckeditor': { + 'ckeditor5-dev-release-tools': { + 'package.json': '' } + } + }, + 'src': { + 'commands': { + 'command-foo.js': '', + 'command-bar.js': '' }, - 'src': { - 'commands': { - 'command-foo.js': '', - 'command-bar.js': '' - }, - 'ui': { - 'view-foo.js': '', - 'view-bar.js': '' - }, - 'index.js': '' + 'ui': { + 'view-foo.js': '', + 'view-bar.js': '' }, - 'tests': { - '_utils': {}, - 'index.js': '' - } + 'index.js': '' + }, + 'tests': { + '_utils': {}, + 'index.js': '' } } - } ); - - await cleanUpPackages( { - packagesDirectory: 'release' - } ); - - const actualPaths = await getAllPaths(); - - expect( actualPaths ).to.have.members( [ - getPathTo( '.' ), - getPathTo( 'release' ), - getPathTo( 'release/ckeditor5-foo' ), - getPathTo( 'release/ckeditor5-foo/package.json' ), - getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ), - getPathTo( 'release/ckeditor5-foo/README.md' ), - getPathTo( 'release/ckeditor5-foo/LICENSE.md' ), - getPathTo( 'release/ckeditor5-foo/src' ), - getPathTo( 'release/ckeditor5-foo/src/ui' ), - getPathTo( 'release/ckeditor5-foo/src/index.js' ), - getPathTo( 'release/ckeditor5-foo/src/commands' ), - getPathTo( 'release/ckeditor5-foo/src/ui/view-foo.js' ), - getPathTo( 'release/ckeditor5-foo/src/ui/view-bar.js' ), - getPathTo( 'release/ckeditor5-foo/src/commands/command-foo.js' ), - getPathTo( 'release/ckeditor5-foo/src/commands/command-bar.js' ) - ] ); - } ); - - it( 'should remove not matched files, empty directories and `node_modules` - pattern with globs', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo', - files: [ - 'ckeditor5-metadata.json', - 'src/**/*.js' - ] - } ), - 'README.md': '', - 'LICENSE.md': '', - 'ckeditor5-metadata.json': '', - 'docs': { - 'assets': { - 'img': { - 'asset.png': '' - } - }, - 'api': { - 'foo.md': '' - }, - 'features': { - 'foo.md': '' + } + } ); + + await cleanUpPackages( { + packagesDirectory: 'release' + } ); + + const actualPaths = await getAllPaths(); + + expect( actualPaths ).to.have.members( [ + getPathTo( '.' ), + getPathTo( 'release' ), + getPathTo( 'release/ckeditor5-foo' ), + getPathTo( 'release/ckeditor5-foo/package.json' ), + getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ), + getPathTo( 'release/ckeditor5-foo/README.md' ), + getPathTo( 'release/ckeditor5-foo/LICENSE.md' ), + getPathTo( 'release/ckeditor5-foo/src' ), + getPathTo( 'release/ckeditor5-foo/src/ui' ), + getPathTo( 'release/ckeditor5-foo/src/index.js' ), + getPathTo( 'release/ckeditor5-foo/src/commands' ), + getPathTo( 'release/ckeditor5-foo/src/ui/view-foo.js' ), + getPathTo( 'release/ckeditor5-foo/src/ui/view-bar.js' ), + getPathTo( 'release/ckeditor5-foo/src/commands/command-foo.js' ), + getPathTo( 'release/ckeditor5-foo/src/commands/command-bar.js' ) + ] ); + } ); + + it( 'should remove not matched files, empty directories and `node_modules` - pattern with globs', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo', + files: [ + 'ckeditor5-metadata.json', + 'src/**/*.js' + ] + } ), + 'README.md': '', + 'LICENSE.md': '', + 'ckeditor5-metadata.json': '', + 'docs': { + 'assets': { + 'img': { + 'asset.png': '' } }, - 'node_modules': { - '.bin': {}, - '@ckeditor': { - 'ckeditor5-dev-release-tools': { - 'package.json': '' - } + 'api': { + 'foo.md': '' + }, + 'features': { + 'foo.md': '' + } + }, + 'node_modules': { + '.bin': {}, + '@ckeditor': { + 'ckeditor5-dev-release-tools': { + 'package.json': '' } + } + }, + 'src': { + 'commands': { + 'command-foo.js': '', + 'command-foo.js.map': '', + 'command-foo.ts': '', + 'command-bar.js': '', + 'command-bar.js.map': '', + 'command-bar.ts': '' }, - 'src': { - 'commands': { - 'command-foo.js': '', - 'command-foo.js.map': '', - 'command-foo.ts': '', - 'command-bar.js': '', - 'command-bar.js.map': '', - 'command-bar.ts': '' - }, - 'ui': { - 'view-foo.js': '', - 'view-foo.js.map': '', - 'view-foo.ts': '', - 'view-bar.js': '', - 'view-bar.js.map': '', - 'view-bar.ts': '' - }, - 'index.js': '', - 'index.js.map': '', - 'index.ts': '' + 'ui': { + 'view-foo.js': '', + 'view-foo.js.map': '', + 'view-foo.ts': '', + 'view-bar.js': '', + 'view-bar.js.map': '', + 'view-bar.ts': '' }, - 'tests': { - '_utils': {}, - 'index.js': '' - } + 'index.js': '', + 'index.js.map': '', + 'index.ts': '' + }, + 'tests': { + '_utils': {}, + 'index.js': '' } } - } ); - - await cleanUpPackages( { - packagesDirectory: 'release' - } ); - - const actualPaths = await getAllPaths(); - - expect( actualPaths ).to.have.members( [ - getPathTo( '.' ), - getPathTo( 'release' ), - getPathTo( 'release/ckeditor5-foo' ), - getPathTo( 'release/ckeditor5-foo/package.json' ), - getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ), - getPathTo( 'release/ckeditor5-foo/README.md' ), - getPathTo( 'release/ckeditor5-foo/LICENSE.md' ), - getPathTo( 'release/ckeditor5-foo/src' ), - getPathTo( 'release/ckeditor5-foo/src/ui' ), - getPathTo( 'release/ckeditor5-foo/src/index.js' ), - getPathTo( 'release/ckeditor5-foo/src/commands' ), - getPathTo( 'release/ckeditor5-foo/src/ui/view-foo.js' ), - getPathTo( 'release/ckeditor5-foo/src/ui/view-bar.js' ), - getPathTo( 'release/ckeditor5-foo/src/commands/command-foo.js' ), - getPathTo( 'release/ckeditor5-foo/src/commands/command-bar.js' ) - ] ); + } } ); + + await cleanUpPackages( { + packagesDirectory: 'release' + } ); + + const actualPaths = await getAllPaths(); + + expect( actualPaths ).to.have.members( [ + getPathTo( '.' ), + getPathTo( 'release' ), + getPathTo( 'release/ckeditor5-foo' ), + getPathTo( 'release/ckeditor5-foo/package.json' ), + getPathTo( 'release/ckeditor5-foo/ckeditor5-metadata.json' ), + getPathTo( 'release/ckeditor5-foo/README.md' ), + getPathTo( 'release/ckeditor5-foo/LICENSE.md' ), + getPathTo( 'release/ckeditor5-foo/src' ), + getPathTo( 'release/ckeditor5-foo/src/ui' ), + getPathTo( 'release/ckeditor5-foo/src/index.js' ), + getPathTo( 'release/ckeditor5-foo/src/commands' ), + getPathTo( 'release/ckeditor5-foo/src/ui/view-foo.js' ), + getPathTo( 'release/ckeditor5-foo/src/ui/view-bar.js' ), + getPathTo( 'release/ckeditor5-foo/src/commands/command-foo.js' ), + getPathTo( 'release/ckeditor5-foo/src/commands/command-bar.js' ) + ] ); } ); + } ); - describe( 'cleaning `package.json`', () => { - it( 'should read and write `package.json` from each found package', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo' - } ) - }, - 'ckeditor5-bar': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-bar' - } ) - } + describe( 'cleaning `package.json`', () => { + it( 'should read and write `package.json` from each found package', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo' + } ) + }, + 'ckeditor5-bar': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-bar' + } ) } - } ); + } + } ); - await cleanUpPackages( { - packagesDirectory: 'release' - } ); + await cleanUpPackages( { + packagesDirectory: 'release' + } ); - // Reading `package.json`. - expect( stubs.fs.readJson.callCount ).to.equal( 2 ); + // Reading `package.json`. + expect( stubs.readJson ).toHaveBeenCalledTimes( 2 ); - let call = stubs.fs.readJson.getCall( 0 ); + let input = stubs.readJson.mock.calls[ 0 ]; + let call = stubs.readJson.mock.results[ 0 ]; - expect( await call.returnValue ).to.have.property( 'name', 'ckeditor5-foo' ); - expect( upath.normalize( call.args[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); + expect( await call.value ).to.have.property( 'name', 'ckeditor5-foo' ); + expect( upath.normalize( input[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); - call = stubs.fs.readJson.getCall( 1 ); + input = stubs.readJson.mock.calls[ 1 ]; + call = stubs.readJson.mock.results[ 1 ]; - expect( await call.returnValue ).to.have.property( 'name', 'ckeditor5-bar' ); - expect( upath.normalize( call.args[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-bar/package.json' ) ); + expect( await call.value ).to.have.property( 'name', 'ckeditor5-bar' ); + expect( upath.normalize( input[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-bar/package.json' ) ); - // Writing `package.json`. - expect( stubs.fs.writeJson.callCount ).to.equal( 2 ); + // Writing `package.json`. + expect( stubs.writeJson ).toHaveBeenCalledTimes( 2 ); - call = stubs.fs.writeJson.getCall( 0 ); + input = stubs.writeJson.mock.calls[ 0 ]; - expect( upath.normalize( call.args[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); + expect( upath.normalize( input[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); - call = stubs.fs.writeJson.getCall( 1 ); + input = stubs.writeJson.mock.calls[ 1 ]; - expect( upath.normalize( call.args[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-bar/package.json' ) ); - } ); + expect( upath.normalize( input[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-bar/package.json' ) ); + } ); - it( 'should not remove any field from `package.json` if all of them are mandatory', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo', - version: '1.0.0', - description: 'Example package.', - dependencies: { - 'ckeditor5': '^37.1.0' - }, - main: 'src/index.ts' - } ) - } + it( 'should not remove any field from `package.json` if all of them are mandatory', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo', + version: '1.0.0', + description: 'Example package.', + dependencies: { + 'ckeditor5': '^37.1.0' + }, + main: 'src/index.ts' + } ) } - } ); + } + } ); - await cleanUpPackages( { - packagesDirectory: 'release' - } ); + await cleanUpPackages( { + packagesDirectory: 'release' + } ); - expect( stubs.fs.writeJson.callCount ).to.equal( 1 ); + expect( stubs.writeJson ).toHaveBeenCalledTimes( 1 ); - const call = stubs.fs.writeJson.getCall( 0 ); + const input = stubs.writeJson.mock.calls[ 0 ]; - expect( upath.normalize( call.args[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); - expect( call.args[ 1 ] ).to.deep.equal( { - name: 'ckeditor5-foo', - version: '1.0.0', - description: 'Example package.', - dependencies: { - 'ckeditor5': '^37.1.0' - }, - main: 'src/index.ts' - } ); - } ); - - it( 'should remove default unnecessary fields from `package.json`', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo', - version: '1.0.0', - description: 'Example package.', - dependencies: { - 'ckeditor5': '^37.1.0' - }, - devDependencies: { - 'typescript': '^4.8.4' - }, - main: 'src/index.ts', - depcheckIgnore: [ - 'eslint-plugin-ckeditor5-rules' - ], - scripts: { - 'build': 'tsc -p ./tsconfig.json', - 'dll:build': 'webpack' - }, - private: true - } ) - } + expect( upath.normalize( input[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); + expect( input[ 1 ] ).to.deep.equal( { + name: 'ckeditor5-foo', + version: '1.0.0', + description: 'Example package.', + dependencies: { + 'ckeditor5': '^37.1.0' + }, + main: 'src/index.ts' + } ); + } ); + + it( 'should remove default unnecessary fields from `package.json`', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo', + version: '1.0.0', + description: 'Example package.', + dependencies: { + 'ckeditor5': '^37.1.0' + }, + devDependencies: { + 'typescript': '^4.8.4' + }, + main: 'src/index.ts', + depcheckIgnore: [ + 'eslint-plugin-ckeditor5-rules' + ], + scripts: { + 'build': 'tsc -p ./tsconfig.json', + 'dll:build': 'webpack' + }, + private: true + } ) } - } ); + } + } ); - await cleanUpPackages( { - packagesDirectory: 'release' - } ); + await cleanUpPackages( { + packagesDirectory: 'release' + } ); - expect( stubs.fs.writeJson.callCount ).to.equal( 1 ); + expect( stubs.writeJson ).toHaveBeenCalledTimes( 1 ); - const call = stubs.fs.writeJson.getCall( 0 ); + const input = stubs.writeJson.mock.calls[ 0 ]; - expect( upath.normalize( call.args[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); - expect( call.args[ 1 ] ).to.deep.equal( { - name: 'ckeditor5-foo', - version: '1.0.0', - description: 'Example package.', - dependencies: { - 'ckeditor5': '^37.1.0' - }, - main: 'src/index.ts' - } ); - } ); - - it( 'should remove provided unnecessary fields from `package.json`', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo', - author: 'CKEditor 5 Devops Team', - version: '1.0.0', - description: 'Example package.', - dependencies: { - 'ckeditor5': '^37.1.0' - }, - devDependencies: { - 'typescript': '^4.8.4' - }, - main: 'src/index.ts', - depcheckIgnore: [ - 'eslint-plugin-ckeditor5-rules' - ], - scripts: { - 'build': 'tsc -p ./tsconfig.json', - 'dll:build': 'webpack' - } - } ) - } + expect( upath.normalize( input[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); + expect( input[ 1 ] ).to.deep.equal( { + name: 'ckeditor5-foo', + version: '1.0.0', + description: 'Example package.', + dependencies: { + 'ckeditor5': '^37.1.0' + }, + main: 'src/index.ts' + } ); + } ); + + it( 'should remove provided unnecessary fields from `package.json`', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo', + author: 'CKEditor 5 Devops Team', + version: '1.0.0', + description: 'Example package.', + dependencies: { + 'ckeditor5': '^37.1.0' + }, + devDependencies: { + 'typescript': '^4.8.4' + }, + main: 'src/index.ts', + depcheckIgnore: [ + 'eslint-plugin-ckeditor5-rules' + ], + scripts: { + 'build': 'tsc -p ./tsconfig.json', + 'dll:build': 'webpack' + } + } ) } - } ); + } + } ); - await cleanUpPackages( { - packagesDirectory: 'release', - packageJsonFieldsToRemove: [ 'author' ] - } ); + await cleanUpPackages( { + packagesDirectory: 'release', + packageJsonFieldsToRemove: [ 'author' ] + } ); - expect( stubs.fs.writeJson.callCount ).to.equal( 1 ); + expect( stubs.writeJson ).toHaveBeenCalledTimes( 1 ); - const call = stubs.fs.writeJson.getCall( 0 ); + const input = stubs.writeJson.mock.calls[ 0 ]; - expect( upath.normalize( call.args[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); - expect( call.args[ 1 ] ).to.deep.equal( { - name: 'ckeditor5-foo', - version: '1.0.0', - description: 'Example package.', - dependencies: { - 'ckeditor5': '^37.1.0' - }, - devDependencies: { - 'typescript': '^4.8.4' - }, - main: 'src/index.ts', - depcheckIgnore: [ - 'eslint-plugin-ckeditor5-rules' - ], - scripts: { - 'build': 'tsc -p ./tsconfig.json', - 'dll:build': 'webpack' - } - } ); - } ); - - it( 'should keep postinstall hook in `package.json` when preservePostInstallHook is set to true', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - scripts: { - 'postinstall': 'node my-node-script.js', - 'build': 'tsc -p ./tsconfig.json', - 'dll:build': 'webpack' - } - } ) - } + expect( upath.normalize( input[ 0 ] ) ).to.equal( getPathTo( 'release/ckeditor5-foo/package.json' ) ); + expect( input[ 1 ] ).to.deep.equal( { + name: 'ckeditor5-foo', + version: '1.0.0', + description: 'Example package.', + dependencies: { + 'ckeditor5': '^37.1.0' + }, + devDependencies: { + 'typescript': '^4.8.4' + }, + main: 'src/index.ts', + depcheckIgnore: [ + 'eslint-plugin-ckeditor5-rules' + ], + scripts: { + 'build': 'tsc -p ./tsconfig.json', + 'dll:build': 'webpack' + } + } ); + } ); + + it( 'should keep postinstall hook in `package.json` when preservePostInstallHook is set to true', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + scripts: { + 'postinstall': 'node my-node-script.js', + 'build': 'tsc -p ./tsconfig.json', + 'dll:build': 'webpack' + } + } ) } - } ); + } + } ); - await cleanUpPackages( { - packagesDirectory: 'release', - preservePostInstallHook: true - } ); + await cleanUpPackages( { + packagesDirectory: 'release', + preservePostInstallHook: true + } ); + const input = stubs.writeJson.mock.calls[ 0 ]; - const call = stubs.fs.writeJson.getCall( 0 ); + expect( input[ 1 ] ).to.deep.equal( { + scripts: { + 'postinstall': 'node my-node-script.js' + } + } ); + } ); - expect( call.args[ 1 ] ).to.deep.equal( { - scripts: { - 'postinstall': 'node my-node-script.js' - } - } ); - } ); - - it( 'should not remove scripts unless it is explicitly specified in packageJsonFieldsToRemove', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - author: 'author', - scripts: { - 'postinstall': 'node my-node-script.js', - 'build': 'tsc -p ./tsconfig.json', - 'dll:build': 'webpack' - } - } ) - } - } - } ); - - await cleanUpPackages( { - packagesDirectory: 'release', - preservePostInstallHook: true, - packageJsonFieldsToRemove: [ - 'author' - ] - } ); - - const call = stubs.fs.writeJson.getCall( 0 ); - - expect( call.args[ 1 ] ).to.deep.equal( { - scripts: { - 'postinstall': 'node my-node-script.js', - 'build': 'tsc -p ./tsconfig.json', - 'dll:build': 'webpack' - } - } ); - } ); - - it( 'should accept a callback for packageJsonFieldsToRemove', async () => { - mockFs( { - 'release': { - 'ckeditor5-foo': { - 'package.json': JSON.stringify( { - name: 'ckeditor5-foo', - author: 'CKEditor 5 Devops Team', - version: '1.0.0', - description: 'Example package.', - dependencies: { - 'ckeditor5': '^37.1.0' - }, - devDependencies: { - 'typescript': '^4.8.4' - }, - main: 'src/index.ts', - depcheckIgnore: [ - 'eslint-plugin-ckeditor5-rules' - ], - scripts: { - 'postinstall': 'node my-node-script.js', - 'build': 'tsc -p ./tsconfig.json', - 'dll:build': 'webpack' - } - } ) - } + it( 'should not remove scripts unless it is explicitly specified in packageJsonFieldsToRemove', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + author: 'author', + scripts: { + 'postinstall': 'node my-node-script.js', + 'build': 'tsc -p ./tsconfig.json', + 'dll:build': 'webpack' + } + } ) } - } ); - - await cleanUpPackages( { - packagesDirectory: 'release', - packageJsonFieldsToRemove: defaults => [ - ...defaults, - 'author' - ] - } ); - - const call = stubs.fs.writeJson.getCall( 0 ); - - expect( call.args[ 1 ] ).to.deep.equal( { - description: 'Example package.', - main: 'src/index.ts', - name: 'ckeditor5-foo', - version: '1.0.0', - dependencies: { - 'ckeditor5': '^37.1.0' + } + } ); + + await cleanUpPackages( { + packagesDirectory: 'release', + preservePostInstallHook: true, + packageJsonFieldsToRemove: [ + 'author' + ] + } ); + + const input = stubs.writeJson.mock.calls[ 0 ]; + + expect( input[ 1 ] ).to.deep.equal( { + scripts: { + 'postinstall': 'node my-node-script.js', + 'build': 'tsc -p ./tsconfig.json', + 'dll:build': 'webpack' + } + } ); + } ); + + it( 'should accept a callback for packageJsonFieldsToRemove', async () => { + mockFs( { + 'release': { + 'ckeditor5-foo': { + 'package.json': JSON.stringify( { + name: 'ckeditor5-foo', + author: 'CKEditor 5 Devops Team', + version: '1.0.0', + description: 'Example package.', + dependencies: { + 'ckeditor5': '^37.1.0' + }, + devDependencies: { + 'typescript': '^4.8.4' + }, + main: 'src/index.ts', + depcheckIgnore: [ + 'eslint-plugin-ckeditor5-rules' + ], + scripts: { + 'postinstall': 'node my-node-script.js', + 'build': 'tsc -p ./tsconfig.json', + 'dll:build': 'webpack' + } + } ) } - } ); + } + } ); + + await cleanUpPackages( { + packagesDirectory: 'release', + packageJsonFieldsToRemove: defaults => [ + ...defaults, + 'author' + ] + } ); + + const input = stubs.writeJson.mock.calls[ 0 ]; + + expect( input[ 1 ] ).to.deep.equal( { + description: 'Example package.', + main: 'src/index.ts', + name: 'ckeditor5-foo', + version: '1.0.0', + dependencies: { + 'ckeditor5': '^37.1.0' + } } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/commitandtag.js b/packages/ckeditor5-dev-release-tools/tests/tasks/commitandtag.js index 55d4975c1..5b8dfd929 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/commitandtag.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/commitandtag.js @@ -3,65 +3,40 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import shellEscape from 'shell-escape'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import { glob } from 'glob'; +import commitAndTag from '../../lib/tasks/commitandtag.js'; -const { expect } = require( 'chai' ); -const mockery = require( 'mockery' ); -const sinon = require( 'sinon' ); +vi.mock( 'glob' ); +vi.mock( 'shell-escape' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); describe( 'commitAndTag()', () => { - let stubs, commitAndTag; - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - tools: { - shExec: sinon.stub().resolves() - }, - glob: { - glob: sinon.stub().returns( [] ) - }, - shellEscape: sinon.stub().callsFake( v => v[ 0 ] ) - }; - - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { - tools: stubs.tools - } ); - mockery.registerMock( 'glob', stubs.glob ); - mockery.registerMock( 'shell-escape', stubs.shellEscape ); - - commitAndTag = require( '../../lib/tasks/commitandtag' ); - } ); - - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sinon.restore(); + vi.mocked( glob ).mockResolvedValue( [] ); + vi.mocked( shellEscape ).mockImplementation( v => `'${ v[ 0 ] }'` ); } ); it( 'should not create a commit and tag if there are no files modified', async () => { await commitAndTag( {} ); - expect( stubs.tools.shExec.called ).to.equal( false ); + expect( vi.mocked( tools.shExec ) ).not.toHaveBeenCalled(); } ); it( 'should allow to specify custom cwd', async () => { - stubs.glob.glob.resolves( [ 'package.json' ] ); + vi.mocked( glob ).mockResolvedValue( [ 'package.json' ] ); await commitAndTag( { version: '1.0.0', cwd: 'my-cwd' } ); - expect( stubs.tools.shExec.firstCall.args[ 1 ].cwd ).to.deep.equal( 'my-cwd' ); - expect( stubs.tools.shExec.secondCall.args[ 1 ].cwd ).to.deep.equal( 'my-cwd' ); - expect( stubs.tools.shExec.thirdCall.args[ 1 ].cwd ).to.deep.equal( 'my-cwd' ); + expect( vi.mocked( tools.shExec ).mock.calls[ 0 ][ 1 ].cwd ).to.deep.equal( 'my-cwd' ); + expect( vi.mocked( tools.shExec ).mock.calls[ 1 ][ 1 ].cwd ).to.deep.equal( 'my-cwd' ); + expect( vi.mocked( tools.shExec ).mock.calls[ 2 ][ 1 ].cwd ).to.deep.equal( 'my-cwd' ); } ); it( 'should add provided files to git one by one', async () => { - stubs.glob.glob.resolves( [ + vi.mocked( glob ).mockResolvedValue( [ 'package.json', 'README.md', 'packages/custom-package/package.json', @@ -73,31 +48,31 @@ describe( 'commitAndTag()', () => { files: [ 'package.json', 'README.md', 'packages/*/package.json', 'packages/*/README.md' ] } ); - expect( stubs.tools.shExec.callCount ).to.equal( 6 ); - expect( stubs.tools.shExec.getCall( 0 ).args[ 0 ] ).to.equal( 'git add package.json' ); - expect( stubs.tools.shExec.getCall( 1 ).args[ 0 ] ).to.equal( 'git add README.md' ); - expect( stubs.tools.shExec.getCall( 2 ).args[ 0 ] ).to.equal( 'git add packages/custom-package/package.json' ); - expect( stubs.tools.shExec.getCall( 3 ).args[ 0 ] ).to.equal( 'git add packages/custom-package/README.md' ); + expect( vi.mocked( tools.shExec ) ).toHaveBeenCalledTimes( 6 ); + expect( vi.mocked( tools.shExec ).mock.calls[ 0 ][ 0 ] ).to.equal( 'git add \'package.json\'' ); + expect( vi.mocked( tools.shExec ).mock.calls[ 1 ][ 0 ] ).to.equal( 'git add \'README.md\'' ); + expect( vi.mocked( tools.shExec ).mock.calls[ 2 ][ 0 ] ).to.equal( 'git add \'packages/custom-package/package.json\'' ); + expect( vi.mocked( tools.shExec ).mock.calls[ 3 ][ 0 ] ).to.equal( 'git add \'packages/custom-package/README.md\'' ); } ); it( 'should set correct commit message', async () => { - stubs.glob.glob.resolves( [ 'package.json' ] ); + vi.mocked( glob ).mockResolvedValue( [ 'package.json' ] ); await commitAndTag( { version: '1.0.0', packagesDirectory: 'packages' } ); - expect( stubs.tools.shExec.secondCall.args[ 0 ] ).to.equal( 'git commit --message "Release: v1.0.0." --no-verify' ); + expect( vi.mocked( tools.shExec ).mock.calls[ 1 ][ 0 ] ).to.equal( 'git commit --message \'Release: v1.0.0.\' --no-verify' ); } ); it( 'should set correct tag', async () => { - stubs.glob.glob.resolves( [ 'package.json' ] ); + vi.mocked( glob ).mockResolvedValue( [ 'package.json' ] ); await commitAndTag( { version: '1.0.0', packagesDirectory: 'packages' } ); - expect( stubs.tools.shExec.thirdCall.args[ 0 ] ).to.equal( 'git tag v1.0.0' ); + expect( vi.mocked( tools.shExec ).mock.calls[ 2 ][ 0 ] ).to.equal( 'git tag \'v1.0.0\'' ); } ); it( 'should escape arguments passed to a shell command', async () => { - stubs.glob.glob.resolves( [ + vi.mocked( glob ).mockResolvedValue( [ 'package.json', 'README.md', 'packages/custom-package/package.json', @@ -109,11 +84,12 @@ describe( 'commitAndTag()', () => { files: [ 'package.json', 'README.md', 'packages/*/package.json', 'packages/*/README.md' ] } ); - expect( stubs.shellEscape.callCount ).to.equal( 5 ); - expect( stubs.shellEscape.getCall( 0 ).args[ 0 ] ).to.deep.equal( [ 'package.json' ] ); - expect( stubs.shellEscape.getCall( 1 ).args[ 0 ] ).to.deep.equal( [ 'README.md' ] ); - expect( stubs.shellEscape.getCall( 2 ).args[ 0 ] ).to.deep.equal( [ 'packages/custom-package/package.json' ] ); - expect( stubs.shellEscape.getCall( 3 ).args[ 0 ] ).to.deep.equal( [ 'packages/custom-package/README.md' ] ); - expect( stubs.shellEscape.getCall( 4 ).args[ 0 ] ).to.deep.equal( [ '1.0.0' ] ); + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledTimes( 6 ); + expect( vi.mocked( shellEscape ).mock.calls[ 0 ][ 0 ] ).to.deep.equal( [ 'package.json' ] ); + expect( vi.mocked( shellEscape ).mock.calls[ 1 ][ 0 ] ).to.deep.equal( [ 'README.md' ] ); + expect( vi.mocked( shellEscape ).mock.calls[ 2 ][ 0 ] ).to.deep.equal( [ 'packages/custom-package/package.json' ] ); + expect( vi.mocked( shellEscape ).mock.calls[ 3 ][ 0 ] ).to.deep.equal( [ 'packages/custom-package/README.md' ] ); + expect( vi.mocked( shellEscape ).mock.calls[ 4 ][ 0 ] ).to.deep.equal( [ 'Release: v1.0.0.' ] ); + expect( vi.mocked( shellEscape ).mock.calls[ 5 ][ 0 ] ).to.deep.equal( [ 'v1.0.0' ] ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/creategithubrelease.js b/packages/ckeditor5-dev-release-tools/tests/tasks/creategithubrelease.js index f8ebb7fee..2f1ac7bec 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/creategithubrelease.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/creategithubrelease.js @@ -3,132 +3,119 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); - -describe( 'dev-release-tools/tasks', () => { - describe( 'createGithubRelease()', () => { - let options, stubs, constructorOptionsCapture, createGithubRelease; - - beforeEach( () => { - options = { - token: 'abc123', - version: '1.3.5', - repositoryOwner: 'ckeditor', - repositoryName: 'ckeditor5-dev', - description: 'Very important release.' +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import createGithubRelease from '../../lib/tasks/creategithubrelease.js'; +import * as transformCommitUtils from '../../lib/utils/transformcommitutils.js'; + +const stubs = vi.hoisted( () => ( { + constructor: vi.fn(), + getLatestRelease: vi.fn(), + createRelease: vi.fn() +} ) ); + +vi.mock( '@octokit/rest', () => ( { + Octokit: class { + constructor( ...args ) { + stubs.constructor( ...args ); + + this.repos = { + getLatestRelease: stubs.getLatestRelease, + createRelease: stubs.createRelease }; + } + } +} ) ); - stubs = { - octokit: { - repos: { - createRelease: sinon.stub().resolves(), - getLatestRelease: sinon.stub().rejects( { - status: 404 - } ) - } - } - }; +vi.mock( '../../lib/utils/transformcommitutils.js' ); - class Octokit { - constructor( options ) { - constructorOptionsCapture = options; +describe( 'createGithubRelease()', () => { + let options; - this.repos = stubs.octokit.repos; - } - } + beforeEach( () => { + options = { + token: 'abc123', + version: '1.3.5', + repositoryOwner: 'ckeditor', + repositoryName: 'ckeditor5-dev', + description: 'Very important release.' + }; - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); + stubs.getLatestRelease.mockRejectedValue( { status: 404 } ); + stubs.createRelease.mockResolvedValue(); - mockery.registerMock( '@octokit/rest', { Octokit } ); + vi.mocked( transformCommitUtils.getRepositoryUrl ).mockReturnValue( 'https://github.com/ckeditor/ckeditor5-dev' ); + } ); - createGithubRelease = require( '../../lib/tasks/creategithubrelease' ); - } ); + it( 'should be a function', () => { + expect( createGithubRelease ).to.be.a( 'function' ); + } ); - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sinon.restore(); - } ); + it( 'creates new Octokit instance with correct arguments', async () => { + await createGithubRelease( options ); - it( 'should be a function', () => { - expect( createGithubRelease ).to.be.a( 'function' ); - } ); + expect( stubs.constructor ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + version: '3.0.0', + auth: 'token abc123' + } ) ); + } ); - it( 'creates new Octokit instance with correct arguments', async () => { - await createGithubRelease( options ); + it( 'resolves a url to the created page', async () => { + const url = await createGithubRelease( options ); - expect( constructorOptionsCapture ).to.deep.equal( { - version: '3.0.0', - auth: 'token abc123' - } ); - } ); + expect( url ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev/releases/tag/v1.3.5' ); + } ); - it( 'resolves a url to the created page', async () => { - const url = await createGithubRelease( options ); + it( 'creates a non-prerelease page when passing a major.minor.patch version', async () => { + await createGithubRelease( options ); - expect( url ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev/releases/tag/v1.3.5' ); - } ); + const createReleaseMock = stubs.createRelease; - it( 'creates a non-prerelease page when passing a major.minor.patch version', async () => { - await createGithubRelease( options ); - - expect( stubs.octokit.repos.createRelease.callCount ).to.equal( 1 ); - expect( stubs.octokit.repos.createRelease.getCall( 0 ).args.length ).to.equal( 1 ); - expect( stubs.octokit.repos.createRelease.getCall( 0 ).args[ 0 ] ).to.deep.equal( { - tag_name: 'v1.3.5', - owner: 'ckeditor', - repo: 'ckeditor5-dev', - body: 'Very important release.', - prerelease: false - } ); - } ); + expect( createReleaseMock ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + tag_name: 'v1.3.5', + owner: 'ckeditor', + repo: 'ckeditor5-dev', + body: 'Very important release.', + prerelease: false + } ) ); + } ); - it( 'creates a prerelease page when passing a major.minor.patch-prerelease version', async () => { - options.version = '1.3.5-alpha.0'; - await createGithubRelease( options ); - - expect( stubs.octokit.repos.createRelease.callCount ).to.equal( 1 ); - expect( stubs.octokit.repos.createRelease.getCall( 0 ).args.length ).to.equal( 1 ); - expect( stubs.octokit.repos.createRelease.getCall( 0 ).args[ 0 ] ).to.deep.equal( { - tag_name: 'v1.3.5-alpha.0', - owner: 'ckeditor', - repo: 'ckeditor5-dev', - body: 'Very important release.', - prerelease: true - } ); - } ); + it( 'creates a prerelease page when passing a major.minor.patch-prerelease version', async () => { + options.version = '1.3.5-alpha.0'; + await createGithubRelease( options ); - it( 'creates a new release if the previous release version are different', async () => { - stubs.octokit.repos.getLatestRelease.resolves( { - data: { - tag_name: 'v1.3.4' - } - } ); + const createReleaseMock = stubs.createRelease; - await createGithubRelease( options ); + expect( createReleaseMock ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + tag_name: 'v1.3.5-alpha.0', + owner: 'ckeditor', + repo: 'ckeditor5-dev', + body: 'Very important release.', + prerelease: true + } ) ); + } ); - expect( stubs.octokit.repos.createRelease.callCount ).to.equal( 1 ); + it( 'creates a new release if the previous release version are different', async () => { + stubs.getLatestRelease.mockResolvedValue( { + data: { + tag_name: 'v1.3.4' + } } ); - it( 'does not create a new release if the previous release version are the same', async () => { - stubs.octokit.repos.getLatestRelease.resolves( { - data: { - tag_name: 'v1.3.5' - } - } ); + await createGithubRelease( options ); - const url = await createGithubRelease( options ); + expect( stubs.createRelease ).toHaveBeenCalledOnce(); + } ); - expect( url ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev/releases/tag/v1.3.5' ); - expect( stubs.octokit.repos.createRelease.callCount ).to.equal( 0 ); + it( 'does not create a new release if the previous release version are the same', async () => { + stubs.getLatestRelease.mockResolvedValue( { + data: { + tag_name: 'v1.3.5' + } } ); + + const url = await createGithubRelease( options ); + + expect( url ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev/releases/tag/v1.3.5' ); + expect( stubs.createRelease ).not.toHaveBeenCalled(); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/preparerepository.js b/packages/ckeditor5-dev-release-tools/tests/tasks/preparerepository.js index a40b64e47..f2bd4bb61 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/preparerepository.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/preparerepository.js @@ -3,115 +3,181 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'fs-extra'; +import { glob } from 'glob'; +import prepareRepository from '../../lib/tasks/preparerepository.js'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +vi.mock( 'fs-extra' ); +vi.mock( 'glob' ); -describe( 'dev-release-tools/tasks', () => { - describe( 'prepareRepository()', () => { - const packages = [ - 'ckeditor5-core', - 'ckeditor5-utils' - ]; +describe( 'prepareRepository()', () => { + const packages = [ + 'ckeditor5-core', + 'ckeditor5-utils' + ]; - let options, stubs, prepareRepository; + let options; - beforeEach( () => { - options = { - outputDirectory: 'release' - }; + beforeEach( () => { + options = { + outputDirectory: 'release' + }; - function stubReject( stubName, args ) { - if ( args.length === 0 ) { - throw new Error( `Stub "${ stubName }" expected to receive an argument.` ); - } + vi.spyOn( process, 'cwd' ).mockReturnValue( 'current/working/dir' ); + } ); - throw new Error( `No output configured for stub "${ stubName }" with the following args: ${ JSON.stringify( args ) }` ); + it( 'should be a function', () => { + expect( prepareRepository ).to.be.a( 'function' ); + } ); + + it( 'should do nothing if neither "rootPackage" or "packagesDirectory" options are defined', async () => { + await prepareRepository( options ); + + expect( vi.mocked( fs ).copy ).not.toHaveBeenCalled(); + expect( vi.mocked( fs ).ensureDir ).not.toHaveBeenCalled(); + expect( vi.mocked( fs ).writeJson ).not.toHaveBeenCalled(); + } ); + + it( 'should ensure the existence of the output directory', async () => { + vi.mocked( fs ).readdir.mockResolvedValue( [] ); + options.packagesDirectory = 'packages'; + + await prepareRepository( options ); + + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalled(); + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalledWith( 'current/working/dir/release' ); + } ); + + it( 'should throw if the output directory is not empty', async () => { + vi.mocked( fs ).readdir.mockImplementation( input => { + if ( input === 'current/working/dir/release' ) { + return Promise.resolve( [ 'someFile.txt' ] ); } - stubs = { - fs: { - copy: sinon.stub().resolves(), - ensureDir: sinon.stub().resolves(), - writeJson: sinon.stub().resolves(), + return Promise.resolve( [] ); + } ); - // These stubs will reject calls without predefined arguments. - lstat: sinon.stub().callsFake( ( ...args ) => stubReject( 'fs.lstat', args ) ), - exists: sinon.stub().callsFake( ( ...args ) => stubReject( 'fs.exists', args ) ), - readdir: sinon.stub().callsFake( ( ...args ) => stubReject( 'fs.readdir', args ) ) - }, - glob: { - sync: sinon.stub().callsFake( ( ...args ) => stubReject( 'glob.sync', args ) ) - }, - lstat: { - isDir: { - isDirectory: sinon.stub().returns( true ) - }, - isNotDir: { - isDirectory: sinon.stub().returns( false ) - } + options.packagesDirectory = 'packages'; + + await prepareRepository( options ) + .then( + () => { + throw new Error( 'Expected to throw.' ); }, - process: { - cwd: sinon.stub( process, 'cwd' ).returns( 'current/working/dir' ) + err => { + expect( err.message ).to.equal( 'Output directory is not empty: "current/working/dir/release".' ); } - }; + ); + + expect( vi.mocked( fs ).readdir ).toHaveBeenCalled(); + expect( vi.mocked( fs ).readdir ).toHaveBeenCalledWith( 'current/working/dir/release' ); + } ); - stubs.fs.readdir.withArgs( 'current/working/dir/release' ).resolves( [] ); - stubs.fs.readdir.withArgs( 'current/working/dir/packages' ).resolves( packages ); + it( 'should use the "cwd" option if provided instead of the default "process.cwd()" value', async () => { + vi.mocked( fs ).readdir.mockResolvedValue( [] ); + options.cwd = 'something/different/than/process/cwd'; + options.packagesDirectory = 'packages'; + + await prepareRepository( options ); + + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalled(); + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalledWith( 'something/different/than/process/cwd/release' ); + } ); + + it( 'should normalize Windows slashes "\\" from "process.cwd()"', async () => { + vi.mocked( fs ).readdir.mockResolvedValue( [] ); + process.cwd.mockReturnValue( 'C:\\windows\\working\\dir' ); + options.packagesDirectory = 'packages'; + + await prepareRepository( options ); + + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalled(); + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalledWith( 'C:/windows/working/dir/release' ); + } ); - stubs.glob.sync.withArgs( [ 'src/*.js', 'CHANGELOG.md' ] ).returns( [ + describe( 'root package processing', () => { + beforeEach( () => { + vi.mocked( fs ).readdir.mockResolvedValue( [] ); + + vi.mocked( glob ).mockResolvedValue( [ 'current/working/dir/src/core.js', 'current/working/dir/src/utils.js', 'current/working/dir/CHANGELOG.md' ] ); + } ); - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'fs-extra', stubs.fs ); - mockery.registerMock( 'glob', stubs.glob ); + it( 'should create "package.json" file in the root package with provided values', async () => { + options.rootPackageJson = { + name: 'ckeditor5', + description: 'Description.', + keywords: [ 'foo', 'bar', 'baz' ], + files: [ 'src/*.js', 'CHANGELOG.md' ] + }; - prepareRepository = require( '../../lib/tasks/preparerepository' ); - } ); + await prepareRepository( options ); - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sinon.restore(); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledExactlyOnceWith( + 'current/working/dir/release/ckeditor5/package.json', + expect.objectContaining( { + name: 'ckeditor5', + description: 'Description.', + keywords: [ 'foo', 'bar', 'baz' ], + files: [ 'src/*.js', 'CHANGELOG.md' ] + } ), + expect.objectContaining( { + spaces: 2, + EOL: '\n' + } ) + ); } ); - it( 'should be a function', () => { - expect( prepareRepository ).to.be.a( 'function' ); - } ); + it( 'should create a flat output file structure for a scoped package', async () => { + options.rootPackageJson = { + name: '@ckeditor/ckeditor5-example', + files: [ 'src/*.js', 'CHANGELOG.md' ] + }; - it( 'should do nothing if neither "rootPackage" or "packagesDirectory" options are defined', async () => { await prepareRepository( options ); - expect( stubs.fs.copy.callCount ).to.equal( 0 ); - expect( stubs.fs.ensureDir.callCount ).to.equal( 0 ); - expect( stubs.fs.writeJson.callCount ).to.equal( 0 ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledExactlyOnceWith( + 'current/working/dir/release/ckeditor5-example/package.json', + expect.any( Object ), + expect.any( Object ) + ); } ); - it( 'should ensure the existence of the output directory', async () => { - stubs.fs.readdir.withArgs( 'current/working/dir/packages' ).resolves( [] ); - options.packagesDirectory = 'packages'; + it( 'should copy specified files of the root package', async () => { + options.rootPackageJson = { + name: 'ckeditor5', + description: '', + keywords: [ 'foo', 'bar', 'baz' ], + files: [ 'src/*.js', 'CHANGELOG.md' ] + }; await prepareRepository( options ); - expect( stubs.fs.ensureDir.callCount ).to.equal( 1 ); - expect( stubs.fs.ensureDir.getCall( 0 ).args.length ).to.equal( 1 ); - expect( stubs.fs.ensureDir.getCall( 0 ).args[ 0 ] ).to.equal( 'current/working/dir/release' ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/src/core.js', + 'current/working/dir/release/ckeditor5/src/core.js' + ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/src/utils.js', + 'current/working/dir/release/ckeditor5/src/utils.js' + ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/CHANGELOG.md', + 'current/working/dir/release/ckeditor5/CHANGELOG.md' + ); } ); - it( 'should throw if the output directory is not empty', async () => { - stubs.fs.readdir.withArgs( 'current/working/dir/release' ).resolves( [ 'someFile.txt' ] ); - stubs.fs.readdir.withArgs( 'current/working/dir/packages' ).resolves( [] ); - options.packagesDirectory = 'packages'; + it( 'should throw if "rootPackageJson" is missing the "name" field', async () => { + options.rootPackageJson = { + description: '', + keywords: [ 'foo', 'bar', 'baz' ], + files: [ 'src/*.js', 'CHANGELOG.md' ] + }; await prepareRepository( options ) .then( @@ -119,237 +185,159 @@ describe( 'dev-release-tools/tasks', () => { throw new Error( 'Expected to throw.' ); }, err => { - expect( err.message ).to.equal( 'Output directory is not empty: "current/working/dir/release".' ); + expect( err.message ).to.equal( '"rootPackageJson" option object must have a "name" field.' ); } ); - expect( stubs.fs.ensureDir.callCount ).to.equal( 1 ); - expect( stubs.fs.readdir.callCount ).to.equal( 1 ); - expect( stubs.fs.copy.callCount ).to.equal( 0 ); - } ); - - it( 'should use the "cwd" option if provided instead of the default "process.cwd()" value', async () => { - stubs.fs.readdir.withArgs( 'custom/working/dir/release' ).resolves( [] ); - stubs.fs.readdir.withArgs( 'custom/working/dir/packages' ).resolves( [] ); - options.cwd = 'custom/working/dir'; - options.packagesDirectory = 'packages'; - - await prepareRepository( options ); - - expect( stubs.fs.ensureDir.callCount ).to.equal( 1 ); - expect( stubs.fs.ensureDir.getCall( 0 ).args.length ).to.equal( 1 ); - expect( stubs.fs.ensureDir.getCall( 0 ).args[ 0 ] ).to.equal( 'custom/working/dir/release' ); + expect( vi.mocked( fs ).writeJson ).not.toHaveBeenCalled(); + expect( vi.mocked( fs ).copy ).not.toHaveBeenCalled(); } ); - it( 'should normalize Windows slashes "\\" from "process.cwd()"', async () => { - stubs.fs.readdir.withArgs( 'windows/working/dir/release' ).resolves( [] ); - stubs.fs.readdir.withArgs( 'windows/working/dir/packages' ).resolves( [] ); - stubs.process.cwd.returns( 'windows\\working\\dir' ); - options.packagesDirectory = 'packages'; + it( 'should throw if "rootPackageJson" is missing the "files" field', async () => { + options.rootPackageJson = { + name: 'ckeditor5', + description: '', + keywords: [ 'foo', 'bar', 'baz' ] + }; - await prepareRepository( options ); + await prepareRepository( options ) + .then( + () => { + throw new Error( 'Expected to throw.' ); + }, + err => { + expect( err.message ).to.equal( '"rootPackageJson" option object must have a "files" field.' ); + } + ); - expect( stubs.fs.ensureDir.callCount ).to.equal( 1 ); - expect( stubs.fs.ensureDir.getCall( 0 ).args.length ).to.equal( 1 ); - expect( stubs.fs.ensureDir.getCall( 0 ).args[ 0 ] ).to.equal( 'windows/working/dir/release' ); + expect( vi.mocked( fs ).writeJson ).not.toHaveBeenCalled(); + expect( vi.mocked( fs ).copy ).not.toHaveBeenCalled(); } ); + } ); - describe( 'root package processing', () => { - it( 'should create "package.json" file in the root package with provided values', async () => { - options.rootPackageJson = { - name: 'CKEditor5', - description: 'Foo bar baz.', - keywords: [ 'foo', 'bar', 'baz' ], - files: [ 'src/*.js', 'CHANGELOG.md' ] - }; - - await prepareRepository( options ); + describe( 'monorepository packages processing', () => { + beforeEach( () => { + vi.mocked( fs ).readdir.mockImplementation( input => { + if ( input.endsWith( 'release' ) ) { + return Promise.resolve( [] ); + } - expect( stubs.fs.writeJson.callCount ).to.equal( 1 ); - expect( stubs.fs.writeJson.getCall( 0 ).args.length ).to.equal( 3 ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 0 ] ).to.equal( 'current/working/dir/release/CKEditor5/package.json' ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 1 ] ).to.deep.equal( { - name: 'CKEditor5', - description: 'Foo bar baz.', - keywords: [ 'foo', 'bar', 'baz' ], - files: [ 'src/*.js', 'CHANGELOG.md' ] - } ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 2 ] ).to.deep.equal( { spaces: 2, EOL: '\n' } ); + return Promise.resolve( packages ); } ); + } ); - it( 'should create a flat output file structure for a scoped package', async () => { - options.rootPackageJson = { - name: '@ckeditor/CKEditor5', - files: [ 'src/*.js', 'CHANGELOG.md' ] - }; - - await prepareRepository( options ); + it( 'should copy files of all packages', async () => { + options.packagesDirectory = 'packages'; - expect( stubs.fs.writeJson.getCall( 0 ).args[ 0 ] ).to.equal( 'current/working/dir/release/CKEditor5/package.json' ); + vi.mocked( fs ).lstat.mockResolvedValue( { + isDirectory: () => true } ); + vi.mocked( fs ).exists.mockResolvedValue( true ); - it( 'should copy specified files of the root package', async () => { - options.rootPackageJson = { - name: 'CKEditor5', - description: 'Foo bar baz.', - keywords: [ 'foo', 'bar', 'baz' ], - files: [ 'src/*.js', 'CHANGELOG.md' ] - }; - - await prepareRepository( options ); + await prepareRepository( options ); - expect( stubs.fs.copy.callCount ).to.equal( 3 ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/packages/ckeditor5-core', + 'current/working/dir/release/ckeditor5-core' + ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/packages/ckeditor5-utils', + 'current/working/dir/release/ckeditor5-utils' + ); + } ); - expect( stubs.fs.copy.getCall( 0 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 0 ).args[ 0 ] ).to.equal( 'current/working/dir/src/core.js' ); - expect( stubs.fs.copy.getCall( 0 ).args[ 1 ] ).to.equal( 'current/working/dir/release/CKEditor5/src/core.js' ); + it( 'should not copy non-directories', async () => { + packages.push( 'textFile.txt' ); + options.packagesDirectory = 'packages'; - expect( stubs.fs.copy.getCall( 1 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 1 ).args[ 0 ] ).to.equal( 'current/working/dir/src/utils.js' ); - expect( stubs.fs.copy.getCall( 1 ).args[ 1 ] ).to.equal( 'current/working/dir/release/CKEditor5/src/utils.js' ); + vi.mocked( fs ).lstat.mockImplementation( input => { + // Paths looking like a file are treated as files. + if ( input.match( /\.[a-z]+$/ ) ) { + return Promise.resolve( { + isDirectory: () => false + } ); + } - expect( stubs.fs.copy.getCall( 2 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 2 ).args[ 0 ] ).to.equal( 'current/working/dir/CHANGELOG.md' ); - expect( stubs.fs.copy.getCall( 2 ).args[ 1 ] ).to.equal( 'current/working/dir/release/CKEditor5/CHANGELOG.md' ); + return Promise.resolve( { + isDirectory: () => true + } ); } ); + vi.mocked( fs ).exists.mockResolvedValue( true ); - it( 'should throw if "rootPackageJson" is missing the "name" field', async () => { - options.rootPackageJson = { - description: 'Foo bar baz.', - keywords: [ 'foo', 'bar', 'baz' ], - files: [ 'src/*.js', 'CHANGELOG.md' ] - }; - - await prepareRepository( options ) - .then( - () => { - throw new Error( 'Expected to throw.' ); - }, - err => { - expect( err.message ).to.equal( '"rootPackageJson" option object must have a "name" field.' ); - } - ); - - expect( stubs.fs.writeJson.callCount ).to.equal( 0 ); - expect( stubs.fs.copy.callCount ).to.equal( 0 ); - } ); + await prepareRepository( options ); - it( 'should throw if "rootPackageJson" is missing the "files" field', async () => { - options.rootPackageJson = { - name: 'CKEditor5', - description: 'Foo bar baz.', - keywords: [ 'foo', 'bar', 'baz' ] - }; - - await prepareRepository( options ) - .then( - () => { - throw new Error( 'Expected to throw.' ); - }, - err => { - expect( err.message ).to.equal( '"rootPackageJson" option object must have a "files" field.' ); - } - ); - - expect( stubs.fs.writeJson.callCount ).to.equal( 0 ); - expect( stubs.fs.copy.callCount ).to.equal( 0 ); - } ); + expect( vi.mocked( fs ).lstat ).toHaveBeenCalledWith( 'current/working/dir/packages/textFile.txt' ); + + expect( vi.mocked( fs ).copy ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/packages/ckeditor5-core', + 'current/working/dir/release/ckeditor5-core' + ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/packages/ckeditor5-utils', + 'current/working/dir/release/ckeditor5-utils' + ); } ); - describe( 'monorepository packages processing', () => { - it( 'should copy files of all packages', async () => { - options.packagesDirectory = 'packages'; - - stubs.fs.lstat.withArgs( 'current/working/dir/packages/ckeditor5-core' ).resolves( stubs.lstat.isDir ); - stubs.fs.lstat.withArgs( 'current/working/dir/packages/ckeditor5-utils' ).resolves( stubs.lstat.isDir ); - stubs.fs.exists.withArgs( 'current/working/dir/packages/ckeditor5-core/package.json' ).resolves( true ); - stubs.fs.exists.withArgs( 'current/working/dir/packages/ckeditor5-utils/package.json' ).resolves( true ); - - await prepareRepository( options ); - - expect( stubs.fs.copy.callCount ).to.equal( 2 ); - - expect( stubs.fs.copy.getCall( 0 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 0 ).args[ 0 ] ).to.equal( 'current/working/dir/packages/ckeditor5-core' ); - expect( stubs.fs.copy.getCall( 0 ).args[ 1 ] ).to.equal( 'current/working/dir/release/ckeditor5-core' ); + it( 'should not copy directories that do not have the "package.json" file', async () => { + options.packagesDirectory = 'packages'; - expect( stubs.fs.copy.getCall( 1 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 1 ).args[ 0 ] ).to.equal( 'current/working/dir/packages/ckeditor5-utils' ); - expect( stubs.fs.copy.getCall( 1 ).args[ 1 ] ).to.equal( 'current/working/dir/release/ckeditor5-utils' ); + vi.mocked( fs ).lstat.mockResolvedValue( { + isDirectory: () => true } ); - it( 'should not copy non-directories', async () => { - options.packagesDirectory = 'packages'; - - stubs.fs.readdir.withArgs( 'current/working/dir/packages' ).resolves( [ ...packages, 'textFile.txt' ] ); - - stubs.fs.lstat.withArgs( 'current/working/dir/packages/ckeditor5-core' ).resolves( stubs.lstat.isDir ); - stubs.fs.lstat.withArgs( 'current/working/dir/packages/ckeditor5-utils' ).resolves( stubs.lstat.isDir ); - stubs.fs.lstat.withArgs( 'current/working/dir/packages/textFile.txt' ).resolves( stubs.lstat.isNotDir ); - stubs.fs.exists.withArgs( 'current/working/dir/packages/ckeditor5-core/package.json' ).resolves( true ); - stubs.fs.exists.withArgs( 'current/working/dir/packages/ckeditor5-utils/package.json' ).resolves( true ); - - await prepareRepository( options ); - - expect( stubs.fs.copy.callCount ).to.equal( 2 ); - - expect( stubs.fs.copy.getCall( 0 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 0 ).args[ 0 ] ).to.equal( 'current/working/dir/packages/ckeditor5-core' ); - expect( stubs.fs.copy.getCall( 0 ).args[ 1 ] ).to.equal( 'current/working/dir/release/ckeditor5-core' ); + vi.mocked( fs ).exists.mockImplementation( input => { + if ( input === 'current/working/dir/packages/ckeditor5-core/package.json' ) { + return Promise.resolve( true ); + } - expect( stubs.fs.copy.getCall( 1 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 1 ).args[ 0 ] ).to.equal( 'current/working/dir/packages/ckeditor5-utils' ); - expect( stubs.fs.copy.getCall( 1 ).args[ 1 ] ).to.equal( 'current/working/dir/release/ckeditor5-utils' ); + return Promise.resolve( false ); } ); - it( 'should not copy directories that do not have the "package.json" file', async () => { - options.packagesDirectory = 'packages'; - - stubs.fs.lstat.withArgs( 'current/working/dir/packages/ckeditor5-core' ).resolves( stubs.lstat.isDir ); - stubs.fs.lstat.withArgs( 'current/working/dir/packages/ckeditor5-utils' ).resolves( stubs.lstat.isDir ); - stubs.fs.exists.withArgs( 'current/working/dir/packages/ckeditor5-core/package.json' ).resolves( true ); - stubs.fs.exists.withArgs( 'current/working/dir/packages/ckeditor5-utils/package.json' ).resolves( false ); + await prepareRepository( options ); - await prepareRepository( options ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/packages/ckeditor5-core', + 'current/working/dir/release/ckeditor5-core' + ); + } ); - expect( stubs.fs.copy.callCount ).to.equal( 1 ); + it( 'should copy only the specified packages if the "packagesToCopy" option is provided', async () => { + options.packagesDirectory = 'packages'; + options.packagesToCopy = [ 'ckeditor5-core' ]; - expect( stubs.fs.copy.getCall( 0 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 0 ).args[ 0 ] ).to.equal( 'current/working/dir/packages/ckeditor5-core' ); - expect( stubs.fs.copy.getCall( 0 ).args[ 1 ] ).to.equal( 'current/working/dir/release/ckeditor5-core' ); + vi.mocked( fs ).lstat.mockResolvedValue( { + isDirectory: () => true } ); + vi.mocked( fs ).exists.mockResolvedValue( true ); - it( 'should copy only the specified packages if the "packagesToCopy" option is provided', async () => { - options.packagesDirectory = 'packages'; - options.packagesToCopy = [ 'ckeditor5-core' ]; - - stubs.fs.lstat.withArgs( 'current/working/dir/packages/ckeditor5-core' ).resolves( stubs.lstat.isDir ); - stubs.fs.exists.withArgs( 'current/working/dir/packages/ckeditor5-core/package.json' ).resolves( true ); + await prepareRepository( options ); - await prepareRepository( options ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/packages/ckeditor5-core', + 'current/working/dir/release/ckeditor5-core' + ); + } ); - expect( stubs.fs.copy.callCount ).to.equal( 1 ); + it( 'should allow copying nested packages via the "packagesToCopy" option', async () => { + options.packagesDirectory = 'packages'; + options.packagesToCopy = [ 'nested/ckeditor5-nested' ]; - expect( stubs.fs.copy.getCall( 0 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 0 ).args[ 0 ] ).to.equal( 'current/working/dir/packages/ckeditor5-core' ); - expect( stubs.fs.copy.getCall( 0 ).args[ 1 ] ).to.equal( 'current/working/dir/release/ckeditor5-core' ); + vi.mocked( fs ).lstat.mockResolvedValue( { + isDirectory: () => true } ); + vi.mocked( fs ).exists.mockResolvedValue( true ); - it( 'should allow copying nested packages via the "packagesToCopy" option', async () => { - options.packagesDirectory = 'packages'; - options.packagesToCopy = [ 'nested/ckeditor5-nested' ]; - - stubs.fs.lstat.withArgs( 'current/working/dir/packages/nested/ckeditor5-nested' ).resolves( stubs.lstat.isDir ); - stubs.fs.exists.withArgs( 'current/working/dir/packages/nested/ckeditor5-nested/package.json' ).resolves( true ); - - await prepareRepository( options ); - - expect( stubs.fs.copy.callCount ).to.equal( 1 ); + await prepareRepository( options ); - expect( stubs.fs.copy.getCall( 0 ).args.length ).to.equal( 2 ); - expect( stubs.fs.copy.getCall( 0 ).args[ 0 ] ).to.equal( 'current/working/dir/packages/nested/ckeditor5-nested' ); - expect( stubs.fs.copy.getCall( 0 ).args[ 1 ] ).to.equal( 'current/working/dir/release/nested/ckeditor5-nested' ); - } ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs ).copy ).toHaveBeenCalledWith( + 'current/working/dir/packages/nested/ckeditor5-nested', + 'current/working/dir/release/nested/ckeditor5-nested' + ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/publishpackages.js b/packages/ckeditor5-dev-release-tools/tests/tasks/publishpackages.js index 99d0c507e..d44cb7d71 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/publishpackages.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/publishpackages.js @@ -3,412 +3,367 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); -const upath = require( 'upath' ); - -describe( 'dev-release-tools/tasks', () => { - describe( 'publishPackages()', () => { - let publishPackages, sandbox, stubs; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - glob: { - glob: sandbox.stub().resolves( [] ) - }, - assertNpmAuthorization: sandbox.stub().resolves(), - assertPackages: sandbox.stub().resolves(), - assertNpmTag: sandbox.stub().resolves(), - assertFilesToPublish: sandbox.stub().resolves(), - executeInParallel: sandbox.stub().resolves(), - publishPackageOnNpmCallback: sandbox.stub().resolves() - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'glob', stubs.glob ); - mockery.registerMock( '../utils/assertnpmauthorization', stubs.assertNpmAuthorization ); - mockery.registerMock( '../utils/assertpackages', stubs.assertPackages ); - mockery.registerMock( '../utils/assertnpmtag', stubs.assertNpmTag ); - mockery.registerMock( '../utils/assertfilestopublish', stubs.assertFilesToPublish ); - mockery.registerMock( '../utils/executeinparallel', stubs.executeInParallel ); - mockery.registerMock( '../utils/publishpackageonnpmcallback', stubs.publishPackageOnNpmCallback ); - - publishPackages = require( '../../lib/tasks/publishpackages' ); - } ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import upath from 'upath'; +import { glob } from 'glob'; +import assertNpmAuthorization from '../../lib/utils/assertnpmauthorization.js'; +import assertPackages from '../../lib/utils/assertpackages.js'; +import assertNpmTag from '../../lib/utils/assertnpmtag.js'; +import assertFilesToPublish from '../../lib/utils/assertfilestopublish.js'; +import executeInParallel from '../../lib/utils/executeinparallel.js'; +import publishPackageOnNpmCallback from '../../lib/utils/publishpackageonnpmcallback.js'; +import publishPackages from '../../lib/tasks/publishpackages.js'; + +vi.mock( 'glob' ); +vi.mock( '../../lib/utils/assertnpmauthorization.js' ); +vi.mock( '../../lib/utils/assertpackages.js' ); +vi.mock( '../../lib/utils/assertnpmtag.js' ); +vi.mock( '../../lib/utils/assertfilestopublish.js' ); +vi.mock( '../../lib/utils/executeinparallel.js' ); +vi.mock( '../../lib/utils/publishpackageonnpmcallback.js' ); + +describe( 'publishPackages()', () => { + beforeEach( () => { + vi.mocked( glob ).mockResolvedValue( [] ); + vi.mocked( assertNpmAuthorization ).mockResolvedValue(); + vi.mocked( assertPackages ).mockResolvedValue(); + vi.mocked( assertNpmTag ).mockResolvedValue(); + vi.mocked( assertFilesToPublish ).mockResolvedValue(); + vi.mocked( executeInParallel ).mockResolvedValue(); + vi.mocked( publishPackageOnNpmCallback ).mockResolvedValue(); + } ); - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); + it( 'should not throw if all assertion passes', async () => { + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' } ); + } ); - it( 'should not throw if all assertion passes', () => { - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ); + it( 'should read the package directory (default `cwd`)', async () => { + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' } ); - it( 'should read the package directory (default `cwd`)', () => { - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( () => { - expect( stubs.glob.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.equal( '*/' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'cwd', upath.join( process.cwd(), 'packages' ) ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'absolute', true ); - } ); - } ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + '*/', + expect.objectContaining( { + cwd: upath.join( process.cwd(), 'packages' ), + absolute: true + } ) ); + } ); + + it( 'should read the package directory (custom `cwd`)', async () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/work/project' ); - it( 'should read the package directory (custom `cwd`)', () => { - sandbox.stub( process, 'cwd' ).returns( '/work/project' ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( () => { - expect( stubs.glob.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.equal( '*/' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'cwd', '/work/project/packages' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'absolute', true ); - } ); + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' } ); - it( 'should assert npm authorization', () => { - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( () => { - expect( stubs.assertNpmAuthorization.callCount ).to.equal( 1 ); - expect( stubs.assertNpmAuthorization.firstCall.args[ 0 ] ).to.equal( 'pepe' ); - } ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + '*/', + expect.objectContaining( { + cwd: '/work/project/packages', + absolute: true + } ) ); + } ); + + it( 'should assert npm authorization', async () => { + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' } ); - it( 'should throw if npm authorization assertion failed', () => { - stubs.assertNpmAuthorization.throws( new Error( 'You must be logged to npm as "pepe" to execute this release step.' ) ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'You must be logged to npm as "pepe" to execute this release step.' - ); - } ); + expect( vi.mocked( assertNpmAuthorization ) ).toHaveBeenCalledExactlyOnceWith( 'pepe' ); + } ); + + it( 'should throw if npm authorization assertion failed', async () => { + vi.mocked( assertNpmAuthorization ).mockRejectedValue( + new Error( 'You must be logged to npm as "pepe" to execute this release step.' ) + ); + + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'fake-pepe' + } ) ).rejects.toThrow( 'You must be logged to npm as "pepe" to execute this release step.' ); + } ); + + it( 'should assert that each found directory is a package', async () => { + vi.mocked( glob ).mockResolvedValue( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' } ); - it( 'should assert that each found directory is a package', () => { - stubs.glob.glob.resolves( [ + expect( vi.mocked( assertPackages ) ).toHaveBeenCalledExactlyOnceWith( + [ '/work/project/packages/ckeditor5-foo', '/work/project/packages/ckeditor5-bar' - ] ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( () => { - expect( stubs.assertPackages.callCount ).to.equal( 1 ); - expect( stubs.assertPackages.firstCall.args[ 0 ] ).to.deep.equal( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ); - expect( stubs.assertPackages.firstCall.args[ 1 ] ).to.deep.equal( { - requireEntryPoint: false, - optionalEntryPointPackages: [] - } ); - } ); + ], + { + requireEntryPoint: false, + optionalEntryPointPackages: [] + } + ); + } ); + + // See: https://github.com/ckeditor/ckeditor5/issues/15127. + it( 'should allow enabling the "package entry point" validator', async () => { + vi.mocked( glob ).mockResolvedValue( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + requireEntryPoint: true, + optionalEntryPointPackages: [ + 'ckeditor5-foo' + ] } ); - // See: https://github.com/ckeditor/ckeditor5/issues/15127. - it( 'should allow enabling the "package entry point" validator', () => { - stubs.glob.glob.resolves( [ + expect( vi.mocked( assertPackages ) ).toHaveBeenCalledExactlyOnceWith( + [ '/work/project/packages/ckeditor5-foo', '/work/project/packages/ckeditor5-bar' - ] ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', + ], + { requireEntryPoint: true, optionalEntryPointPackages: [ 'ckeditor5-foo' ] - } ).then( () => { - expect( stubs.assertPackages.callCount ).to.equal( 1 ); - expect( stubs.assertPackages.firstCall.args[ 0 ] ).to.deep.equal( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ); - expect( stubs.assertPackages.firstCall.args[ 1 ] ).to.deep.equal( { - requireEntryPoint: true, - optionalEntryPointPackages: [ - 'ckeditor5-foo' - ] - } ); - } ); - } ); + } + ); + } ); + + it( 'should throw if package assertion failed', async () => { + vi.mocked( assertPackages ).mockRejectedValue( + new Error( 'The "package.json" file is missing in the "ckeditor5-foo" package.' ) + ); + + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' + } ) ).rejects.toThrow( 'The "package.json" file is missing in the "ckeditor5-foo" package.' ); + } ); + + it( 'should assert that each required file exists in the package directory (no optional entries)', async () => { + vi.mocked( glob ).mockResolvedValue( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ); - it( 'should throw if package assertion failed', () => { - stubs.assertPackages.throws( new Error( 'The "package.json" file is missing in the "ckeditor5-foo" package.' ) ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'The "package.json" file is missing in the "ckeditor5-foo" package.' - ); - } ); + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' } ); - it( 'should assert that each required file exists in the package directory (no optional entries)', () => { - stubs.glob.glob.resolves( [ + expect( vi.mocked( assertFilesToPublish ) ).toHaveBeenCalledExactlyOnceWith( + [ '/work/project/packages/ckeditor5-foo', '/work/project/packages/ckeditor5-bar' - ] ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( () => { - expect( stubs.assertFilesToPublish.callCount ).to.equal( 1 ); - expect( stubs.assertFilesToPublish.firstCall.args[ 0 ] ).to.deep.equal( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ); - expect( stubs.assertFilesToPublish.firstCall.args[ 1 ] ).to.equal( null ); - } ); + ], + null + ); + } ); + + it( 'should assert that each required file exists in the package directory (with optional entries)', async () => { + vi.mocked( glob ).mockResolvedValue( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + optionalEntries: { + 'ckeditor5-foo': [ 'src' ] + } } ); - it( 'should assert that each required file exists in the package directory (with optional entries)', () => { - stubs.glob.glob.resolves( [ + expect( vi.mocked( assertFilesToPublish ) ).toHaveBeenCalledExactlyOnceWith( + [ '/work/project/packages/ckeditor5-foo', '/work/project/packages/ckeditor5-bar' - ] ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - optionalEntries: { - 'ckeditor5-foo': [ 'src' ] - } - } ).then( () => { - expect( stubs.assertFilesToPublish.callCount ).to.equal( 1 ); - expect( stubs.assertFilesToPublish.firstCall.args[ 0 ] ).to.deep.equal( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ); - expect( stubs.assertFilesToPublish.firstCall.args[ 1 ] ).to.deep.equal( { - 'ckeditor5-foo': [ 'src' ] - } ); - } ); - } ); + ], + { + 'ckeditor5-foo': [ 'src' ] + } + ); + } ); + + it( 'should throw if not all required files exist in the package directory', async () => { + vi.mocked( assertFilesToPublish ).mockRejectedValue( + new Error( 'Missing files in "ckeditor5-foo" package for entries: "src"' ) + ); - it( 'should throw if not all required files exist in the package directory', () => { - stubs.assertFilesToPublish.throws( new Error( 'Missing files in "ckeditor5-foo" package for entries: "src"' ) ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'Missing files in "ckeditor5-foo" package for entries: "src"' - ); - } ); + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' + } ) ).rejects.toThrow( 'Missing files in "ckeditor5-foo" package for entries: "src"' ); + } ); + + it( 'should assert that version tag matches the npm tag (default npm tag)', async () => { + vi.mocked( glob ).mockResolvedValue( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' } ); - it( 'should assert that version tag matches the npm tag (default npm tag)', () => { - stubs.glob.glob.resolves( [ + expect( vi.mocked( assertNpmTag ) ).toHaveBeenCalledExactlyOnceWith( + [ '/work/project/packages/ckeditor5-foo', '/work/project/packages/ckeditor5-bar' - ] ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( () => { - expect( stubs.assertNpmTag.callCount ).to.equal( 1 ); - expect( stubs.assertNpmTag.firstCall.args[ 0 ] ).to.deep.equal( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ); - expect( stubs.assertNpmTag.firstCall.args[ 1 ] ).to.equal( 'staging' ); - } ); + ], + 'staging' + ); + } ); + + it( 'should assert that version tag matches the npm tag (custom npm tag)', async () => { + vi.mocked( glob ).mockResolvedValue( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + npmTag: 'nightly' } ); - it( 'should assert that version tag matches the npm tag (custom npm tag)', () => { - stubs.glob.glob.resolves( [ + expect( vi.mocked( assertNpmTag ) ).toHaveBeenCalledExactlyOnceWith( + [ '/work/project/packages/ckeditor5-foo', '/work/project/packages/ckeditor5-bar' - ] ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - npmTag: 'nightly' - } ).then( () => { - expect( stubs.assertNpmTag.callCount ).to.equal( 1 ); - expect( stubs.assertNpmTag.firstCall.args[ 0 ] ).to.deep.equal( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ); - expect( stubs.assertNpmTag.firstCall.args[ 1 ] ).to.equal( 'nightly' ); - } ); - } ); + ], + 'nightly' + ); + } ); + + it( 'should throw if version tag does not match the npm tag', async () => { + vi.mocked( assertNpmTag ).mockRejectedValue( + new Error( 'The version tag "rc" from "ckeditor5-foo" package does not match the npm tag "staging".' ) + ); + + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' + } ) ).rejects.toThrow( 'The version tag "rc" from "ckeditor5-foo" package does not match the npm tag "staging".' ); + } ); - it( 'should throw if version tag does not match the npm tag', () => { - stubs.assertNpmTag.throws( - new Error( 'The version tag "rc" from "ckeditor5-foo" package does not match the npm tag "staging".' ) - ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'The version tag "rc" from "ckeditor5-foo" package does not match the npm tag "staging".' - ); - } ); + it( 'should pass parameters for publishing packages', async () => { + const listrTask = {}; + const abortController = new AbortController(); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + npmTag: 'nightly', + listrTask, + signal: abortController.signal, + concurrency: 3, + cwd: '/home/cwd' } ); - it( 'should pass parameters for publishing packages', () => { - const listrTask = {}; - const abortController = new AbortController(); - const taskToExecute = stubs.publishPackageOnNpmCallback; - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - npmTag: 'nightly', - listrTask, - signal: abortController.signal, - concurrency: 3, - cwd: '/home/cwd' - } ).then( () => { - expect( stubs.executeInParallel.callCount ).to.equal( 1 ); - expect( stubs.executeInParallel.firstCall.args[ 0 ] ).to.have.property( 'packagesDirectory', 'packages' ); - expect( stubs.executeInParallel.firstCall.args[ 0 ] ).to.have.property( 'listrTask', listrTask ); - expect( stubs.executeInParallel.firstCall.args[ 0 ] ).to.have.property( 'taskToExecute', taskToExecute ); - expect( stubs.executeInParallel.firstCall.args[ 0 ] ).to.have.deep.property( 'taskOptions', { npmTag: 'nightly' } ); - expect( stubs.executeInParallel.firstCall.args[ 0 ] ).to.have.property( 'signal', abortController.signal ); - expect( stubs.executeInParallel.firstCall.args[ 0 ] ).to.have.property( 'concurrency', 3 ); - expect( stubs.executeInParallel.firstCall.args[ 0 ] ).to.have.property( 'cwd', '/home/cwd' ); - } ); + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledExactlyOnceWith( { + packagesDirectory: 'packages', + listrTask, + taskToExecute: publishPackageOnNpmCallback, + taskOptions: { npmTag: 'nightly' }, + signal: abortController.signal, + concurrency: 3, + cwd: '/home/cwd' } ); + } ); - it( 'should publish packages on npm if confirmation callback is not set', () => { - const listrTask = {}; + it( 'should publish packages on npm if confirmation callback is not set', async () => { + const listrTask = {}; - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - listrTask - } ).then( () => { - expect( stubs.executeInParallel.callCount ).to.equal( 1 ); - } ); + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask } ); - it( 'should publish packages on npm if synchronous confirmation callback returns truthy value', () => { - const confirmationCallback = sandbox.stub().returns( true ); - const listrTask = {}; - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - confirmationCallback, - listrTask - } ).then( () => { - expect( stubs.executeInParallel.callCount ).to.equal( 1 ); - expect( confirmationCallback.callCount ).to.equal( 1 ); - } ); - } ); + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledOnce(); + } ); - it( 'should publish packages on npm if asynchronous confirmation callback returns truthy value', () => { - const confirmationCallback = sandbox.stub().resolves( true ); - const listrTask = {}; - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - confirmationCallback, - listrTask - } ).then( () => { - expect( stubs.executeInParallel.callCount ).to.equal( 1 ); - expect( confirmationCallback.callCount ).to.equal( 1 ); - } ); + it( 'should publish packages on npm if synchronous confirmation callback returns truthy value', async () => { + const confirmationCallback = vi.fn().mockReturnValue( true ); + const listrTask = {}; + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + confirmationCallback, + listrTask } ); - it( 'should not publish packages on npm if synchronous confirmation callback returns falsy value', () => { - const confirmationCallback = sandbox.stub().returns( false ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - confirmationCallback - } ).then( () => { - expect( stubs.executeInParallel.callCount ).to.equal( 0 ); - expect( confirmationCallback.callCount ).to.equal( 1 ); - } ); + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledOnce(); + } ); + + it( 'should publish packages on npm if asynchronous confirmation callback returns truthy value', async () => { + const confirmationCallback = vi.fn().mockResolvedValue( true ); + const listrTask = {}; + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + confirmationCallback, + listrTask } ); - it( 'should not publish packages on npm if asynchronous confirmation callback returns falsy value', () => { - const confirmationCallback = sandbox.stub().resolves( false ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - confirmationCallback - } ).then( () => { - expect( stubs.executeInParallel.callCount ).to.equal( 0 ); - expect( confirmationCallback.callCount ).to.equal( 1 ); - } ); + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledOnce(); + } ); + + it( 'should not publish packages on npm if synchronous confirmation callback returns falsy value', async () => { + const confirmationCallback = vi.fn().mockReturnValue( false ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + confirmationCallback } ); - it( 'should throw if publishing packages on npm failed', () => { - stubs.executeInParallel.throws( new Error( 'Unable to publish "ckeditor5-foo" package.' ) ); - - return publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ).then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( 'Unable to publish "ckeditor5-foo" package.' ); - } ); + expect( vi.mocked( executeInParallel ) ).not.toHaveBeenCalledOnce(); + expect( confirmationCallback ).toHaveBeenCalledOnce(); + } ); + + it( 'should not publish packages on npm if asynchronous confirmation callback returns falsy value', async () => { + const confirmationCallback = vi.fn().mockResolvedValue( false ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + confirmationCallback } ); + + expect( vi.mocked( executeInParallel ) ).not.toHaveBeenCalledOnce(); + expect( confirmationCallback ).toHaveBeenCalledOnce(); + } ); + + it( 'should throw if publishing packages on npm failed', async () => { + vi.mocked( executeInParallel ).mockRejectedValue( + new Error( 'Unable to publish "ckeditor5-foo" package.' ) + ); + + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' + } ) ).rejects.toThrow( + 'Unable to publish "ckeditor5-foo" package.' + ); } ); -} ); +} ) +; diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/push.js b/packages/ckeditor5-dev-release-tools/tests/tasks/push.js index 8caff4b79..fbed66b09 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/push.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/push.js @@ -5,92 +5,66 @@ 'use strict'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); - -describe( 'dev-release-tools/tasks', () => { - describe( 'push()', () => { - let options, stubs, push; - - beforeEach( () => { - options = { - releaseBranch: 'release', - version: '1.3.5', - cwd: 'custom/working/dir' - }; - - stubs = { - devUtils: { - tools: { - shExec: sinon.stub() - } - }, - process: { - cwd: sinon.stub( process, 'cwd' ).returns( 'current/working/dir' ) - }, - shellEscape: sinon.stub().callsFake( v => v[ 0 ] ) - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', stubs.devUtils ); - mockery.registerMock( 'shell-escape', stubs.shellEscape ); - - push = require( '../../lib/tasks/push' ); - } ); - - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sinon.restore(); - } ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import shellEscape from 'shell-escape'; +import push from '../../lib/tasks/push.js'; + +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( 'shell-escape' ); + +describe( 'push()', () => { + let options; + beforeEach( () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( 'current/working/dir' ); + vi.mocked( shellEscape ).mockImplementation( v => v[ 0 ] ); + vi.mocked( tools.shExec ).mockResolvedValue(); + + options = { + releaseBranch: 'release', + version: '1.3.5', + cwd: 'custom-modified/working/dir' + }; + } ); - it( 'should be a function', () => { - expect( push ).to.be.a( 'function' ); - } ); + it( 'should be a function', () => { + expect( push ).to.be.a( 'function' ); + } ); - it( 'should execute command with correct arguments', async () => { - stubs.devUtils.tools.shExec.resolves(); - await push( options ); + it( 'should execute command with correct arguments', async () => { + await push( options ); - expect( stubs.devUtils.tools.shExec.callCount ).to.equal( 1 ); - expect( stubs.devUtils.tools.shExec.getCall( 0 ).args.length ).to.equal( 2 ); - expect( stubs.devUtils.tools.shExec.getCall( 0 ).args[ 0 ] ).to.equal( 'git push origin release v1.3.5' ); - expect( stubs.devUtils.tools.shExec.getCall( 0 ).args[ 1 ] ).to.deep.equal( { - cwd: 'custom/working/dir', + expect( vi.mocked( tools.shExec ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( tools.shExec ) ).toHaveBeenCalledWith( + 'git push origin release v1.3.5', + { + cwd: 'custom-modified/working/dir', verbosity: 'error', async: true - } ); - } ); + } + ); + } ); - it( 'should use "process.cwd()" if the "cwd" option was not used', async () => { - delete options.cwd; + it( 'should use "process.cwd()" if the "cwd" option was not used', async () => { + delete options.cwd; - stubs.devUtils.tools.shExec.resolves(); - await push( options ); + await push( options ); - expect( stubs.devUtils.tools.shExec.callCount ).to.equal( 1 ); - expect( stubs.devUtils.tools.shExec.getCall( 0 ).args.length ).to.equal( 2 ); - expect( stubs.devUtils.tools.shExec.getCall( 0 ).args[ 0 ] ).to.equal( 'git push origin release v1.3.5' ); - expect( stubs.devUtils.tools.shExec.getCall( 0 ).args[ 1 ] ).to.deep.equal( { + expect( vi.mocked( tools.shExec ) ).toHaveBeenCalledExactlyOnceWith( + 'git push origin release v1.3.5', + { cwd: 'current/working/dir', verbosity: 'error', async: true - } ); - } ); + } + ); + } ); - it( 'should escape arguments passed to a shell command', async () => { - stubs.devUtils.tools.shExec.resolves(); - await push( options ); + it( 'should escape arguments passed to a shell command', async () => { + await push( options ); - expect( stubs.shellEscape.callCount ).to.equal( 2 ); - expect( stubs.shellEscape.firstCall.firstArg ).to.deep.equal( [ 'release' ] ); - expect( stubs.shellEscape.secondCall.firstArg ).to.deep.equal( [ '1.3.5' ] ); - } ); + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledWith( [ 'release' ] ); + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledWith( [ '1.3.5' ] ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/reassignnpmtags.js b/packages/ckeditor5-dev-release-tools/tests/tasks/reassignnpmtags.js index 774bb1a52..addc44512 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/reassignnpmtags.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/reassignnpmtags.js @@ -5,200 +5,198 @@ 'use strict'; -const { expect } = require( 'chai' ); -const mockery = require( 'mockery' ); -const sinon = require( 'sinon' ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import columns from 'cli-columns'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import shellEscape from 'shell-escape'; +import assertNpmAuthorization from '../../lib/utils/assertnpmauthorization.js'; +import reassignNpmTags from '../../lib/tasks/reassignnpmtags.js'; + +const stubs = vi.hoisted( () => { + const values = { + spinner: { + start: vi.fn(), + increase: vi.fn(), + finish: vi.fn() + }, + exec: vi.fn(), + chalk: { + bold: vi.fn( () => stubs.chalk ), + green: vi.fn( input => input ), + yellow: vi.fn( input => input ), + red: vi.fn( input => input ) + } + }; -describe( 'reassignNpmTags()', () => { - let stubs, reassignNpmTags; + // To make `chalk.bold.yellow.red()` working. + for ( const rootKey of Object.keys( values.chalk ) ) { + for ( const nestedKey of Object.keys( values.chalk ) ) { + values.chalk[ rootKey ][ nestedKey ] = values.chalk[ nestedKey ]; + } + } - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); + return values; +} ); - stubs = { - tools: { - createSpinner: sinon.stub().callsFake( () => { - return stubs.spinner; - } ) - }, - assertNpmAuthorization: sinon.stub().resolves( true ), - spinner: { - start: sinon.stub(), - increase: sinon.stub(), - finish: sinon.stub() - }, - chalk: { - get bold() { - return stubs.chalk; - }, - green: sinon.stub().callsFake( str => str ), - yellow: sinon.stub().callsFake( str => str ), - red: sinon.stub().callsFake( str => str ) - }, - columns: sinon.stub(), - console: { - log: sinon.stub( console, 'log' ) - }, - util: { - promisify: sinon.stub().callsFake( () => stubs.exec ) - }, - exec: sinon.stub(), - shellEscape: sinon.stub().callsFake( v => v[ 0 ] ) - }; - - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { tools: stubs.tools } ); - mockery.registerMock( '../utils/assertnpmauthorization', stubs.assertNpmAuthorization ); - mockery.registerMock( 'cli-columns', stubs.columns ); - mockery.registerMock( 'chalk', stubs.chalk ); - mockery.registerMock( 'util', stubs.util ); - mockery.registerMock( 'shell-escape', stubs.shellEscape ); - - reassignNpmTags = require( '../../lib/tasks/reassignnpmtags' ); - } ); +vi.stubGlobal( 'console', { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn() +} ); - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sinon.restore(); +vi.mock( '@ckeditor/ckeditor5-dev-utils', () => ( { + tools: { + createSpinner: vi.fn( () => stubs.spinner ) + } +} ) ); +vi.mock( 'util', () => ( { + default: { + promisify: vi.fn( () => stubs.exec ) + } +} ) ); +vi.mock( 'shell-escape' ); +vi.mock( 'cli-columns' ); +vi.mock( 'chalk', () => ( { + default: stubs.chalk +} ) ); +vi.mock( 'shell-escape' ); +vi.mock( '../../lib/utils/assertnpmauthorization.js' ); + +describe( 'reassignNpmTags()', () => { + beforeEach( () => { + vi.mocked( shellEscape ).mockImplementation( v => v[ 0 ] ); + vi.mocked( assertNpmAuthorization ).mockResolvedValue( true ); } ); it( 'should throw an error when assertNpmAuthorization throws error', async () => { - stubs.assertNpmAuthorization.throws( new Error( 'User not logged in error' ) ); - const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); - - try { - await reassignNpmTags( { npmOwner: 'correct-npm-user', version: '1.0.1', packages: [ 'package1' ] } ); - throw new Error( 'Expected to throw' ); - } catch ( e ) { - expect( e.message ).to.equal( 'User not logged in error' ); - } + vi.mocked( assertNpmAuthorization ).mockRejectedValue( + new Error( 'User not logged in error' ) + ); + await expect( reassignNpmTags( { npmOwner: 'correct-npm-user', version: '1.0.1', packages: [ 'package1' ] } ) ) + .rejects.toThrow( 'User not logged in error' ); - expect( npmDistTagAdd.callCount ).to.equal( 0 ); + expect( stubs.exec ).not.toHaveBeenCalled(); } ); it( 'should skip updating tags when provided version matches existing version for tag latest', async () => { - stubs.columns.returns( 'package1 | package2' ); - stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ).throws( new Error( 'is already set to version' ) ); + vi.mocked( columns ).mockReturnValue( 'package1 | package2' ); + stubs.exec.mockRejectedValue( new Error( 'is already set to version' ) ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.0', packages: [ 'package1', 'package2' ] } ); - expect( stubs.console.log.firstCall.args[ 0 ] ).to.equal( '⬇️ Packages skipped:' ); - expect( stubs.console.log.secondCall.args[ 0 ] ).to.deep.equal( 'package1 | package2' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( console ).log ).toHaveBeenCalledWith( '⬇️ Packages skipped:' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledWith( 'package1 | package2' ); } ); it( 'should update tags when tag latest for provided version does not yet exist', async () => { - const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ).resolves( { stdout: '+latest' } ); + stubs.exec.mockResolvedValue( { stdout: '+latest' } ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1', 'package2' ] } ); - expect( npmDistTagAdd.firstCall.args[ 0 ] ).to.equal( 'npm dist-tag add package1@1.0.1 latest' ); - expect( npmDistTagAdd.secondCall.args[ 0 ] ).to.equal( 'npm dist-tag add package2@1.0.1 latest' ); + expect( stubs.exec ).toHaveBeenCalledTimes( 2 ); + expect( stubs.exec ).toHaveBeenCalledWith( 'npm dist-tag add package1@1.0.1 latest' ); + expect( stubs.exec ).toHaveBeenCalledWith( 'npm dist-tag add package2@1.0.1 latest' ); } ); it( 'should continue updating packages even if first package update fails', async () => { - const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); - npmDistTagAdd.onFirstCall().throws( new Error( 'Npm error while updating tag.' ) ); + stubs.exec + .mockRejectedValueOnce( new Error( 'is already set to version' ) ) + .mockResolvedValueOnce( { stdout: '+latest' } ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1', 'package2' ] } ); - expect( npmDistTagAdd.firstCall.args[ 0 ] ).to.equal( 'npm dist-tag add package1@1.0.1 latest' ); - expect( npmDistTagAdd.secondCall.args[ 0 ] ).to.equal( 'npm dist-tag add package2@1.0.1 latest' ); + expect( stubs.exec ).toHaveBeenCalledWith( 'npm dist-tag add package1@1.0.1 latest' ); + expect( stubs.exec ).toHaveBeenCalledWith( 'npm dist-tag add package2@1.0.1 latest' ); } ); it( 'should escape arguments passed to a shell command', async () => { - stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ).resolves( { stdout: '+latest' } ); + stubs.exec.mockResolvedValue( { stdout: '+latest' } ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1' ] } ); - expect( stubs.shellEscape.callCount ).to.equal( 2 ); - expect( stubs.shellEscape.firstCall.firstArg ).to.deep.equal( [ 'package1' ] ); - expect( stubs.shellEscape.secondCall.firstArg ).to.deep.equal( [ '1.0.1' ] ); + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledWith( [ 'package1' ] ); + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledWith( [ '1.0.1' ] ); } ); describe( 'UX', () => { it( 'should create a spinner before starting processing packages', async () => { await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [] } ); - expect( stubs.tools.createSpinner.callCount ).to.equal( 1 ); - expect( stubs.tools.createSpinner.firstCall.args[ 0 ] ).to.equal( 'Reassigning npm tags...' ); - expect( stubs.tools.createSpinner.firstCall.args[ 1 ] ).to.be.an( 'object' ); - expect( stubs.tools.createSpinner.firstCall.args[ 1 ] ).to.have.property( 'total', 0 ); - - expect( stubs.spinner.start.callCount ).to.equal( 1 ); + expect( vi.mocked( tools ).createSpinner ).toHaveBeenCalledExactlyOnceWith( + 'Reassigning npm tags...', + { + total: 0 + } + ); + expect( stubs.spinner.start ).toHaveBeenCalledOnce(); } ); it( 'should increase the spinner counter after successfully processing a package', async () => { await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1' ] } ); - expect( stubs.spinner.increase.callCount ).to.equal( 1 ); + expect( stubs.spinner.increase ).toHaveBeenCalledTimes( 1 ); } ); it( 'should increase the spinner counter after failure processing a package', async () => { - const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); - npmDistTagAdd.onFirstCall().throws( new Error( 'Npm error while updating tag.' ) ); + stubs.exec.mockRejectedValue( new Error( 'is already set to version' ) ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1' ] } ); - expect( stubs.spinner.increase.callCount ).to.equal( 1 ); + expect( stubs.spinner.increase ).toHaveBeenCalledTimes( 1 ); } ); it( 'should finish the spinner once all packages have been processed', async () => { - const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); - npmDistTagAdd.onFirstCall().throws( new Error( 'Npm error while updating tag.' ) ); + stubs.exec + .mockRejectedValueOnce( new Error( 'is already set to version' ) ) + .mockResolvedValueOnce( { stdout: '+latest' } ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1', 'package2' ] } ); - sinon.assert.callOrder( - stubs.spinner.start, - stubs.spinner.increase, - stubs.spinner.increase, - stubs.spinner.finish - ); + expect( stubs.spinner.start ).toHaveBeenCalledTimes( 1 ); + expect( stubs.spinner.increase ).toHaveBeenCalledTimes( 2 ); + expect( stubs.spinner.finish ).toHaveBeenCalledTimes( 1 ); + + expect( stubs.spinner.start ).toHaveBeenCalledBefore( stubs.spinner.increase ); + expect( stubs.spinner.start ).toHaveBeenCalledBefore( stubs.spinner.finish ); + expect( stubs.spinner.increase ).toHaveBeenCalledBefore( stubs.spinner.finish ); } ); it( 'should display skipped packages in a column', async () => { - stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ).throws( new Error( 'is already set to version' ) ); - stubs.columns.returns( '1 | 2 | 3' ); + stubs.exec.mockRejectedValue( new Error( 'is already set to version' ) ); + vi.mocked( columns ).mockReturnValue( '1 | 2 | 3' ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.0', packages: [ 'package1', 'package2' ] } ); - expect( stubs.columns.callCount ).to.equal( 1 ); - expect( stubs.columns.firstCall.args[ 0 ] ).to.be.an( 'array' ); - expect( stubs.columns.firstCall.args[ 0 ] ).to.include( 'package1' ); - expect( stubs.columns.firstCall.args[ 0 ] ).to.include( 'package2' ); - expect( stubs.console.log.callCount ).to.equal( 2 ); - expect( stubs.console.log.firstCall.args[ 0 ] ).to.equal( '⬇️ Packages skipped:' ); - expect( stubs.console.log.secondCall.args[ 0 ] ).to.equal( '1 | 2 | 3' ); + expect( vi.mocked( columns ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( columns ) ).toHaveBeenCalledWith( [ 'package1', 'package2' ] ); + expect( vi.mocked( console ).log ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( console ).log ).toHaveBeenCalledWith( '⬇️ Packages skipped:' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledWith( '1 | 2 | 3' ); } ); it( 'should display processed packages in a column', async () => { - stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ).resolves( { stdout: '+latest' } ); - stubs.columns.returns( '1 | 2 | 3' ); + stubs.exec.mockResolvedValue( { stdout: '+latest' } ); + vi.mocked( columns ).mockReturnValue( '1 | 2 | 3' ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1', 'package2' ] } ); - expect( stubs.columns.callCount ).to.equal( 1 ); - expect( stubs.columns.firstCall.args[ 0 ] ).to.be.an( 'array' ); - expect( stubs.columns.firstCall.args[ 0 ] ).to.include( 'package1' ); - expect( stubs.columns.firstCall.args[ 0 ] ).to.include( 'package2' ); - expect( stubs.console.log.callCount ).to.equal( 2 ); - expect( stubs.console.log.firstCall.args[ 0 ] ).to.equal( '✨ Tags updated:' ); - expect( stubs.console.log.secondCall.args[ 0 ] ).to.equal( '1 | 2 | 3' ); + expect( vi.mocked( columns ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( columns ) ).toHaveBeenCalledWith( [ 'package1', 'package2' ] ); + expect( vi.mocked( console ).log ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( console ).log ).toHaveBeenCalledWith( '✨ Tags updated:' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledWith( '1 | 2 | 3' ); } ); it( 'should display errors found during processing a package', async () => { - const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); - npmDistTagAdd.throws( new Error( 'Npm error while updating tag.' ) ); + stubs.exec.mockRejectedValue( new Error( 'Npm error while updating tag.' ) ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1' ] } ); - expect( stubs.console.log.callCount ).to.equal( 2 ); - expect( stubs.console.log.firstCall.args[ 0 ] ).to.equal( '🐛 Errors found:' ); - expect( stubs.console.log.secondCall.args[ 0 ] ).to.equal( '* Npm error while updating tag.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( console ).log ).toHaveBeenCalledWith( '🐛 Errors found:' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledWith( '* Npm error while updating tag.' ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/updatedependencies.js b/packages/ckeditor5-dev-release-tools/tests/tasks/updatedependencies.js index 88ccccabb..9574d1762 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/updatedependencies.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/updatedependencies.js @@ -3,338 +3,358 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const expect = require( 'chai' ).expect; -const upath = require( 'upath' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); - -describe( 'dev-release-tools/tasks', () => { - describe( 'updateDependencies()', () => { - let updateDependencies, sandbox, stubs; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'fs-extra'; +import { glob } from 'glob'; +import upath from 'upath'; +import updateDependencies from '../../lib/tasks/updatedependencies.js'; + +vi.mock( 'fs-extra' ); +vi.mock( 'glob' ); + +describe( 'updateDependencies()', () => { + beforeEach( () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/work/project' ); + } ); + describe( 'preparing options', () => { beforeEach( () => { - sandbox = sinon.createSandbox(); + vi.mocked( glob ).mockResolvedValue( [] ); + } ); - stubs = { - fs: { - readJson: sandbox.stub(), - writeJson: sandbox.stub() - }, - glob: { - glob: sandbox.stub() - }, - process: { - cwd: sandbox.stub( process, 'cwd' ).returns( '/work/project' ) - } + it( 'should use provided `cwd` to search for packages', async () => { + const options = { + cwd: '/work/another/project' }; - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'fs-extra', stubs.fs ); - mockery.registerMock( 'glob', stubs.glob ); + await updateDependencies( options ); - updateDependencies = require( '../../lib/tasks/updatedependencies' ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ 'package.json' ], + expect.objectContaining( { + cwd: '/work/another/project' + } ) + ); } ); - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); - } ); + it( 'should use `process.cwd()` to search for packages if `cwd` option is not provided', async () => { + await updateDependencies( {} ); - describe( 'preparing options', () => { - beforeEach( () => { - stubs.glob.glob.resolves( [] ); - } ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ 'package.json' ], + expect.objectContaining( { + cwd: '/work/project' + } ) + ); + } ); - it( 'should use provided `cwd` to search for packages', async () => { - const options = { - cwd: '/work/another/project' - }; + it( 'should match only files', async () => { + await updateDependencies( {} ); - await updateDependencies( options ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + expect.any( Array ), + expect.objectContaining( { + nodir: true + } ) + ); + } ); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 1 ] ).to.have.property( 'cwd', '/work/another/project' ); - } ); + it( 'should always receive absolute paths for matched files', async () => { + await updateDependencies( {} ); - it( 'should use `process.cwd()` to search for packages if `cwd` option is not provided', async () => { - await updateDependencies( {} ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + expect.any( Array ), + expect.objectContaining( { + absolute: true + } ) + ); + } ); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 1 ] ).to.have.property( 'cwd', '/work/project' ); + it( 'should use the `packagesDirectory` option for searching for packages in `cwd`', () => { + updateDependencies( { + packagesDirectory: 'packages' } ); - it( 'should match only files', async () => { - await updateDependencies( {} ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ + 'package.json', + 'packages/*/package.json' + ], + expect.any( Object ) + ); + } ); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 1 ] ).to.have.property( 'nodir', true ); - } ); + it( 'should not search for packages if the `packagesDirectory` option is not provided', async () => { + await updateDependencies( {} ); - it( 'should always receive absolute paths for matched files', async () => { - await updateDependencies( {} ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ 'package.json' ], + expect.any( Object ) + ); + } ); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 1 ] ).to.have.property( 'absolute', true ); + it( 'should convert backslashes to slashes from the `packagesDirectory` (Windows-like paths)', async () => { + await updateDependencies( { + packagesDirectory: 'path\\to\\packages\\' } ); - it( 'should search for packages in `cwd` and `packagesDirectory`', () => { - updateDependencies( { - packagesDirectory: 'packages' - } ); - - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 0 ] ).to.deep.equal( [ + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ 'package.json', - 'packages/*/package.json' - ] ); - } ); + 'path/to/packages/*/package.json' + ], + expect.any( Object ) + ); + } ); + } ); - it( 'should search for packages only in `cwd` if `packagesDirectory` option is not provided', async () => { - await updateDependencies( {} ); + describe( 'updating dependencies', () => { + let shouldUpdateVersionCallback; - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 0 ] ).to.deep.equal( [ - 'package.json' - ] ); + beforeEach( () => { + shouldUpdateVersionCallback = vi.fn( packageName => packageName.startsWith( '@ckeditor' ) ); + } ); + + it( 'should read and write `package.json` for each found package', async () => { + vi.mocked( glob ).mockImplementation( patterns => { + const paths = { + 'package.json': [ + '/work/project/package.json' + ], + 'packages/*/package.json': [ + '/work/project/packages/ckeditor5-foo/package.json', + '/work/project/packages/ckeditor5-bar/package.json' + ] + }; + + return Promise.resolve( + patterns.flatMap( pattern => paths[ pattern ] || [] ) + ); } ); - it( 'should convert backslashes to slashes from the `packagesDirectory`', async () => { - await updateDependencies( { - packagesDirectory: 'path\\to\\packages\\' - } ); + vi.mocked( fs ).readJson.mockResolvedValue( {} ); - expect( stubs.glob.glob.calledOnce ).to.equal( true ); - expect( stubs.glob.glob.getCall( 0 ).args[ 0 ] ).to.deep.equal( [ - 'package.json', - 'path/to/packages/*/package.json' - ] ); + await updateDependencies( { + packagesDirectory: 'packages' } ); + + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( '/work/project/package.json' ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-foo/package.json' ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-bar/package.json' ); + + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/work/project/package.json', + expect.any( Object ), + expect.any( Object ) + ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/work/project/packages/ckeditor5-foo/package.json', + expect.any( Object ), + expect.any( Object ) + ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/work/project/packages/ckeditor5-bar/package.json', + expect.any( Object ), + expect.any( Object ) + ); } ); - describe( 'updating dependencies', () => { - let shouldUpdateVersionCallback; + it( 'should allow filtering out packages that do not pass the `packagesDirectoryFilter` callback', async () => { + vi.mocked( glob ).mockImplementation( patterns => { + const paths = { + 'package.json': [ + '/work/project/package.json' + ], + 'packages/*/package.json': [ + '/work/project/packages/ckeditor5-ignore-me/package.json', + '/work/project/packages/ckeditor5-bar/package.json' + ] + }; - beforeEach( () => { - shouldUpdateVersionCallback = sandbox.stub().callsFake( packageName => packageName.startsWith( '@ckeditor' ) ); + return Promise.resolve( + patterns.flatMap( pattern => paths[ pattern ] || [] ) + ); } ); - it( 'should read and write `package.json` for each found package', async () => { - stubs.glob.glob.callsFake( patterns => { - const paths = { - 'package.json': [ - '/work/project/package.json' - ], - 'packages/*/package.json': [ - '/work/project/packages/ckeditor5-foo/package.json', - '/work/project/packages/ckeditor5-bar/package.json' - ] - }; - - return Promise.resolve( - patterns.flatMap( pattern => paths[ pattern ] || [] ) - ); - } ); - - stubs.fs.readJson.resolves( {} ); - - await updateDependencies( { - packagesDirectory: 'packages' - } ); - - expect( stubs.fs.readJson.callCount ).to.equal( 3 ); - expect( stubs.fs.readJson.getCall( 0 ).args[ 0 ] ).to.equal( '/work/project/package.json' ); - expect( stubs.fs.readJson.getCall( 1 ).args[ 0 ] ).to.equal( '/work/project/packages/ckeditor5-foo/package.json' ); - expect( stubs.fs.readJson.getCall( 2 ).args[ 0 ] ).to.equal( '/work/project/packages/ckeditor5-bar/package.json' ); - - expect( stubs.fs.writeJson.callCount ).to.equal( 3 ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 0 ] ).to.equal( '/work/project/package.json' ); - expect( stubs.fs.writeJson.getCall( 1 ).args[ 0 ] ).to.equal( '/work/project/packages/ckeditor5-foo/package.json' ); - expect( stubs.fs.writeJson.getCall( 2 ).args[ 0 ] ).to.equal( '/work/project/packages/ckeditor5-bar/package.json' ); + vi.mocked( fs ).readJson.mockResolvedValue( {} ); + + const directoriesToSkip = [ + 'ckeditor5-ignore-me' + ]; + + await updateDependencies( { + version: '^38.0.0', + packagesDirectory: 'packages', + packagesDirectoryFilter: packageJsonPath => { + return !directoriesToSkip.some( item => { + return upath.dirname( packageJsonPath ).endsWith( item ); + } ); + } } ); - it( 'should allow filtering out packages that do not pass the `packagesDirectoryFilter` callback', async () => { - stubs.glob.glob.callsFake( patterns => { - const paths = { - 'package.json': [ - '/work/project/package.json' - ], - 'packages/*/package.json': [ - '/work/project/packages/ckeditor5-ignore-me/package.json', - '/work/project/packages/ckeditor5-bar/package.json' - ] - }; - - return Promise.resolve( - patterns.flatMap( pattern => paths[ pattern ] || [] ) - ); - } ); - - stubs.fs.readJson.resolves( {} ); - - const directoriesToSkip = [ - 'ckeditor5-ignore-me' - ]; - - await updateDependencies( { - version: '^38.0.0', - packagesDirectory: 'packages', - packagesDirectoryFilter: packageJsonPath => { - return !directoriesToSkip.some( item => { - return upath.dirname( packageJsonPath ).endsWith( item ); - } ); - } - } ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( '/work/project/package.json' ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-bar/package.json' ); + + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/work/project/package.json', + expect.any( Object ), + expect.any( Object ) + ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/work/project/packages/ckeditor5-bar/package.json', + expect.any( Object ), + expect.any( Object ) + ); + } ); - expect( stubs.fs.readJson.callCount ).to.equal( 2 ); - expect( stubs.fs.readJson.getCall( 0 ).args[ 0 ] ).to.equal( '/work/project/package.json' ); - expect( stubs.fs.readJson.getCall( 1 ).args[ 0 ] ).to.equal( '/work/project/packages/ckeditor5-bar/package.json' ); + it( 'should update eligible dependencies from the `dependencies` key', async () => { + vi.mocked( glob ).mockResolvedValue( [ '/work/project/package.json' ] ); - expect( stubs.fs.writeJson.callCount ).to.equal( 2 ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 0 ] ).to.equal( '/work/project/package.json' ); - expect( stubs.fs.writeJson.getCall( 1 ).args[ 0 ] ).to.equal( '/work/project/packages/ckeditor5-bar/package.json' ); + vi.mocked( fs ).readJson.mockResolvedValue( { + dependencies: { + '@ckeditor/ckeditor5-engine': '^37.0.0', + '@ckeditor/ckeditor5-enter': '^37.0.0', + '@ckeditor/ckeditor5-essentials': '^37.0.0', + 'lodash-es': '^4.17.15' + } } ); - it( 'should update eligible dependencies from the `dependencies` key', async () => { - stubs.glob.glob.resolves( [ '/work/project/package.json' ] ); + await updateDependencies( { + version: '^38.0.0', + shouldUpdateVersionCallback + } ); - stubs.fs.readJson.resolves( { - dependencies: { - '@ckeditor/ckeditor5-engine': '^37.0.0', - '@ckeditor/ckeditor5-enter': '^37.0.0', - '@ckeditor/ckeditor5-essentials': '^37.0.0', - 'lodash-es': '^4.17.15' - } - } ); - - await updateDependencies( { - version: '^38.0.0', - shouldUpdateVersionCallback - } ); - - expect( shouldUpdateVersionCallback.callCount ).to.equal( 4 ); - expect( shouldUpdateVersionCallback.getCall( 0 ).args[ 0 ] ).to.equal( '@ckeditor/ckeditor5-engine' ); - expect( shouldUpdateVersionCallback.getCall( 1 ).args[ 0 ] ).to.equal( '@ckeditor/ckeditor5-enter' ); - expect( shouldUpdateVersionCallback.getCall( 2 ).args[ 0 ] ).to.equal( '@ckeditor/ckeditor5-essentials' ); - expect( shouldUpdateVersionCallback.getCall( 3 ).args[ 0 ] ).to.equal( 'lodash-es' ); - - expect( stubs.fs.writeJson.callCount ).to.equal( 1 ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 0 ] ).to.equal( '/work/project/package.json' ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 1 ] ).to.deep.equal( { + expect( shouldUpdateVersionCallback ).toHaveBeenCalledTimes( 4 ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( '@ckeditor/ckeditor5-engine' ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( '@ckeditor/ckeditor5-enter' ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( '@ckeditor/ckeditor5-essentials' ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( 'lodash-es' ); + + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/work/project/package.json', + { dependencies: { '@ckeditor/ckeditor5-engine': '^38.0.0', '@ckeditor/ckeditor5-enter': '^38.0.0', '@ckeditor/ckeditor5-essentials': '^38.0.0', 'lodash-es': '^4.17.15' } - } ); + }, + expect.any( Object ) + ); + } ); + + it( 'should update eligible dependencies from the `devDependencies` key', async () => { + vi.mocked( glob ).mockResolvedValue( [ '/work/project/package.json' ] ); + + vi.mocked( fs ).readJson.mockResolvedValue( { + devDependencies: { + '@ckeditor/ckeditor5-engine': '^37.0.0', + '@ckeditor/ckeditor5-enter': '^37.0.0', + '@ckeditor/ckeditor5-essentials': '^37.0.0', + 'lodash-es': '^4.17.15' + } } ); - it( 'should update eligible dependencies from the `devDependencies` key', async () => { - stubs.glob.glob.resolves( [ '/work/project/package.json' ] ); + await updateDependencies( { + version: '^38.0.0', + shouldUpdateVersionCallback + } ); - stubs.fs.readJson.resolves( { - devDependencies: { - '@ckeditor/ckeditor5-engine': '^37.0.0', - '@ckeditor/ckeditor5-enter': '^37.0.0', - '@ckeditor/ckeditor5-essentials': '^37.0.0', - 'lodash-es': '^4.17.15' - } - } ); - - await updateDependencies( { - version: '^38.0.0', - shouldUpdateVersionCallback - } ); - - expect( shouldUpdateVersionCallback.callCount ).to.equal( 4 ); - expect( shouldUpdateVersionCallback.getCall( 0 ).args[ 0 ] ).to.equal( '@ckeditor/ckeditor5-engine' ); - expect( shouldUpdateVersionCallback.getCall( 1 ).args[ 0 ] ).to.equal( '@ckeditor/ckeditor5-enter' ); - expect( shouldUpdateVersionCallback.getCall( 2 ).args[ 0 ] ).to.equal( '@ckeditor/ckeditor5-essentials' ); - expect( shouldUpdateVersionCallback.getCall( 3 ).args[ 0 ] ).to.equal( 'lodash-es' ); - - expect( stubs.fs.writeJson.callCount ).to.equal( 1 ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 0 ] ).to.equal( '/work/project/package.json' ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 1 ] ).to.deep.equal( { + expect( shouldUpdateVersionCallback ).toHaveBeenCalledTimes( 4 ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( '@ckeditor/ckeditor5-engine' ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( '@ckeditor/ckeditor5-enter' ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( '@ckeditor/ckeditor5-essentials' ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( 'lodash-es' ); + + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/work/project/package.json', + { devDependencies: { '@ckeditor/ckeditor5-engine': '^38.0.0', '@ckeditor/ckeditor5-enter': '^38.0.0', '@ckeditor/ckeditor5-essentials': '^38.0.0', 'lodash-es': '^4.17.15' } - } ); + }, + expect.any( Object ) + ); + } ); + + it( 'should update eligible dependencies from the `peerDependencies` key', async () => { + vi.mocked( glob ).mockResolvedValue( [ '/work/project/package.json' ] ); + + vi.mocked( fs ).readJson.mockResolvedValue( { + peerDependencies: { + '@ckeditor/ckeditor5-engine': '^37.0.0', + '@ckeditor/ckeditor5-enter': '^37.0.0', + '@ckeditor/ckeditor5-essentials': '^37.0.0', + 'lodash-es': '^4.17.15' + } } ); - it( 'should update eligible dependencies from the `peerDependencies` key', async () => { - stubs.glob.glob.resolves( [ '/work/project/package.json' ] ); + await updateDependencies( { + version: '^38.0.0', + shouldUpdateVersionCallback + } ); - stubs.fs.readJson.resolves( { - peerDependencies: { - '@ckeditor/ckeditor5-engine': '^37.0.0', - '@ckeditor/ckeditor5-enter': '^37.0.0', - '@ckeditor/ckeditor5-essentials': '^37.0.0', - 'lodash-es': '^4.17.15' - } - } ); - - await updateDependencies( { - version: '^38.0.0', - shouldUpdateVersionCallback - } ); - - expect( shouldUpdateVersionCallback.callCount ).to.equal( 4 ); - expect( shouldUpdateVersionCallback.getCall( 0 ).args[ 0 ] ).to.equal( '@ckeditor/ckeditor5-engine' ); - expect( shouldUpdateVersionCallback.getCall( 1 ).args[ 0 ] ).to.equal( '@ckeditor/ckeditor5-enter' ); - expect( shouldUpdateVersionCallback.getCall( 2 ).args[ 0 ] ).to.equal( '@ckeditor/ckeditor5-essentials' ); - expect( shouldUpdateVersionCallback.getCall( 3 ).args[ 0 ] ).to.equal( 'lodash-es' ); - - expect( stubs.fs.writeJson.callCount ).to.equal( 1 ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 0 ] ).to.equal( '/work/project/package.json' ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 1 ] ).to.deep.equal( { + expect( shouldUpdateVersionCallback ).toHaveBeenCalledTimes( 4 ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( '@ckeditor/ckeditor5-engine' ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( '@ckeditor/ckeditor5-enter' ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( '@ckeditor/ckeditor5-essentials' ); + expect( shouldUpdateVersionCallback ).toHaveBeenCalledWith( 'lodash-es' ); + + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/work/project/package.json', + { peerDependencies: { '@ckeditor/ckeditor5-engine': '^38.0.0', '@ckeditor/ckeditor5-enter': '^38.0.0', '@ckeditor/ckeditor5-essentials': '^38.0.0', 'lodash-es': '^4.17.15' } - } ); - } ); + }, + expect.any( Object ) + ); + } ); - it( 'should not update any package if `shouldUpdateVersionCallback` callback resolves falsy value', async () => { - stubs.glob.glob.resolves( [ '/work/project/package.json' ] ); + it( 'should not update any package if `shouldUpdateVersionCallback` callback resolves falsy value', async () => { + vi.mocked( glob ).mockResolvedValue( [ '/work/project/package.json' ] ); - stubs.fs.readJson.resolves( { - dependencies: { - '@ckeditor/ckeditor5-engine': '^37.0.0', - '@ckeditor/ckeditor5-enter': '^37.0.0', - '@ckeditor/ckeditor5-essentials': '^37.0.0', - 'lodash-es': '^4.17.15' - } - } ); + vi.mocked( fs ).readJson.mockResolvedValue( { + dependencies: { + '@ckeditor/ckeditor5-engine': '^37.0.0', + '@ckeditor/ckeditor5-enter': '^37.0.0', + '@ckeditor/ckeditor5-essentials': '^37.0.0', + 'lodash-es': '^4.17.15' + } + } ); - await updateDependencies( { - version: '^38.0.0', - shouldUpdateVersionCallback: () => false - } ); + await updateDependencies( { + version: '^38.0.0', + shouldUpdateVersionCallback: () => false + } ); - expect( stubs.fs.writeJson.callCount ).to.equal( 1 ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 0 ] ).to.equal( '/work/project/package.json' ); - expect( stubs.fs.writeJson.getCall( 0 ).args[ 1 ] ).to.deep.equal( { + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/work/project/package.json', + { dependencies: { '@ckeditor/ckeditor5-engine': '^37.0.0', '@ckeditor/ckeditor5-enter': '^37.0.0', '@ckeditor/ckeditor5-essentials': '^37.0.0', 'lodash-es': '^4.17.15' } - } ); - } ); + }, + expect.any( Object ) + ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/updateversions.js b/packages/ckeditor5-dev-release-tools/tests/tasks/updateversions.js index 58a52800d..3eb93c3f6 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/updateversions.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/updateversions.js @@ -5,212 +5,213 @@ 'use strict'; -const upath = require( 'upath' ); -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); - -describe( 'dev-release-tools/release', () => { - let updateVersions, sandbox, stubs; - - describe( 'updateVersions()', () => { - beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - outputJson: sandbox.stub(), - readJson: sandbox.stub().resolves( { version: '1.0.0' } ), - glob: sandbox.stub().resolves( [ '/ckeditor5-dev' ] ), - checkVersionAvailability: sandbox.stub().resolves( true ) - }; - - updateVersions = proxyquire( '../../lib/tasks/updateversions.js', { - 'fs-extra': { - writeJson: stubs.outputJson, - readJson: stubs.readJson - }, - 'glob': { glob: stubs.glob }, - '../utils/checkversionavailability': stubs.checkVersionAvailability - } ); - } ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { glob } from 'glob'; +import upath from 'upath'; +import fs from 'fs-extra'; +import checkVersionAvailability from '../../lib/utils/checkversionavailability.js'; +import updateVersions from '../../lib/tasks/updateversions.js'; + +vi.mock( 'fs-extra' ); +vi.mock( 'glob' ); +vi.mock( '../../lib/utils/checkversionavailability.js' ); + +describe( 'updateVersions()', () => { + beforeEach( () => { + vi.mocked( fs ).readJson.mockResolvedValue( { version: '1.0.0' } ); + vi.mocked( glob ).mockResolvedValue( [ '/ckeditor5-dev' ] ); + vi.mocked( checkVersionAvailability ).mockResolvedValue( true ); + vi.spyOn( process, 'cwd' ).mockReturnValue( '/ckeditor5-dev' ); + } ); - afterEach( () => { - sandbox.restore(); - } ); + it( 'should update the version field in all found packages including the root package', async () => { + vi.mocked( glob ).mockResolvedValue( [ + '/ckeditor5-dev/packages/package1/package.json', + '/ckeditor5-dev/packages/package2/package.json', + '/ckeditor5-dev/packages/package3/package.json', + '/ckeditor5-dev/package.json' + ] ); + + await updateVersions( { version: '1.0.1', packagesDirectory: 'packages' } ); + + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ 'package.json', 'packages/*/package.json' ], + expect.any( Object ) + ); + + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledTimes( 4 ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/ckeditor5-dev/packages/package1/package.json', + { + version: '1.0.1' + }, + expect.any( Object ) + ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/ckeditor5-dev/packages/package2/package.json', + { + version: '1.0.1' + }, + expect.any( Object ) + ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/ckeditor5-dev/packages/package3/package.json', + { + version: '1.0.1' + }, + expect.any( Object ) + ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledWith( + '/ckeditor5-dev/package.json', + { + version: '1.0.1' + }, + expect.any( Object ) + ); + } ); - it( 'should update the version field in all found packages including the root package', async () => { - stubs.glob.resolves( [ - '/ckeditor5-dev/packages/package1/package.json', - '/ckeditor5-dev/packages/package2/package.json', - '/ckeditor5-dev/packages/package3/package.json', - '/ckeditor5-dev/package.json' - ] ); - - await updateVersions( { version: '1.0.1', packagesDirectory: 'packages' } ); - - expect( stubs.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'package.json', 'packages/*/package.json' ] ); - - expect( stubs.outputJson.callCount ).to.equal( 4 ); - expect( stubs.outputJson.getCall( 0 ).args[ 0 ] ).to.contain( '/ckeditor5-dev/packages/package1/package.json' ); - expect( stubs.outputJson.getCall( 0 ).args[ 1 ] ).to.deep.equal( { version: '1.0.1' } ); - expect( stubs.outputJson.getCall( 1 ).args[ 0 ] ).to.contain( '/ckeditor5-dev/packages/package2/package.json' ); - expect( stubs.outputJson.getCall( 1 ).args[ 1 ] ).to.deep.equal( { version: '1.0.1' } ); - expect( stubs.outputJson.getCall( 2 ).args[ 0 ] ).to.contain( '/ckeditor5-dev/packages/package3/package.json' ); - expect( stubs.outputJson.getCall( 2 ).args[ 1 ] ).to.deep.equal( { version: '1.0.1' } ); - expect( stubs.outputJson.getCall( 3 ).args[ 0 ] ).to.equal( '/ckeditor5-dev/package.json' ); - expect( stubs.outputJson.getCall( 3 ).args[ 1 ] ).to.deep.equal( { version: '1.0.1' } ); + it( 'should allow filtering out packages that do not pass the `packagesDirectoryFilter` callback', async () => { + vi.mocked( glob ).mockResolvedValue( [ + '/ckeditor5-dev/packages/package1/package.json', + '/ckeditor5-dev/packages/package-bar/package.json', + '/ckeditor5-dev/packages/package-foo/package.json', + '/ckeditor5-dev/packages/package-number/package.json', + '/ckeditor5-dev/package.json' + ] ); + + const directoriesToSkip = [ + 'package-number' + ]; + + await updateVersions( { + version: '1.0.1', + packagesDirectory: 'packages', + packagesDirectoryFilter: packageJsonPath => { + return !directoriesToSkip.some( item => { + return upath.dirname( packageJsonPath ).endsWith( item ); + } ); + } } ); - it( 'should allow filtering out packages that do not pass the `packagesDirectoryFilter` callback', async () => { - stubs.glob.resolves( [ - '/ckeditor5-dev/packages/package1/package.json', - '/ckeditor5-dev/packages/package-bar/package.json', - '/ckeditor5-dev/packages/package-foo/package.json', - '/ckeditor5-dev/packages/package-number/package.json', - '/ckeditor5-dev/package.json' - ] ); - - const directoriesToSkip = [ - 'package-number' - ]; - - await updateVersions( { - version: '1.0.1', - packagesDirectory: 'packages', - packagesDirectoryFilter: packageJsonPath => { - return !directoriesToSkip.some( item => { - return upath.dirname( packageJsonPath ).endsWith( item ); - } ); - } - } ); - - expect( stubs.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'package.json', 'packages/*/package.json' ] ); - - expect( stubs.outputJson.callCount ).to.equal( 4 ); - expect( stubs.outputJson.getCall( 0 ).args[ 0 ] ).to.contain( '/ckeditor5-dev/packages/package1/package.json' ); - expect( stubs.outputJson.getCall( 0 ).args[ 1 ] ).to.deep.equal( { version: '1.0.1' } ); - expect( stubs.outputJson.getCall( 1 ).args[ 0 ] ).to.contain( '/ckeditor5-dev/packages/package-bar/package.json' ); - expect( stubs.outputJson.getCall( 1 ).args[ 1 ] ).to.deep.equal( { version: '1.0.1' } ); - expect( stubs.outputJson.getCall( 2 ).args[ 0 ] ).to.contain( '/ckeditor5-dev/packages/package-foo/package.json' ); - expect( stubs.outputJson.getCall( 2 ).args[ 1 ] ).to.deep.equal( { version: '1.0.1' } ); - expect( stubs.outputJson.getCall( 3 ).args[ 0 ] ).to.equal( '/ckeditor5-dev/package.json' ); - expect( stubs.outputJson.getCall( 3 ).args[ 1 ] ).to.deep.equal( { version: '1.0.1' } ); - } ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ 'package.json', 'packages/*/package.json' ], + expect.any( Object ) + ); + + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalled(); + expect( vi.mocked( fs ).writeJson ).not.toHaveBeenCalledWith( + '/ckeditor5-dev/packages/package-number/package.json', + { + version: '1.0.1' + }, + expect.any( Object ) + ); + } ); - it( 'should update the version field in the root package when `packagesDirectory` is not provided', async () => { - stubs.glob.resolves( [ '/ckeditor5-dev' ] ); + it( 'should update the version field in the root package when `packagesDirectory` is not provided', async () => { + vi.mocked( glob ).mockResolvedValue( [ '/ckeditor5-dev/package.json' ] ); - await updateVersions( { version: '1.0.1' } ); + await updateVersions( { version: '1.0.1' } ); - expect( stubs.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'package.json' ] ); + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ 'package.json' ], + expect.any( Object ) + ); - expect( stubs.outputJson.callCount ).to.equal( 1 ); - expect( stubs.outputJson.firstCall.args[ 0 ] ).to.contain( '/ckeditor5-dev' ); - expect( stubs.outputJson.firstCall.args[ 1 ] ).to.deep.equal( { version: '1.0.1' } ); - } ); + expect( vi.mocked( fs ).writeJson ).toHaveBeenCalledExactlyOnceWith( + '/ckeditor5-dev/package.json', + { + version: '1.0.1' + }, + expect.any( Object ) + ); + } ); - it( 'should throw an error when the version is already in use', async () => { - stubs.readJson.resolves( { version: '1.0.0', name: 'stub-package' } ); - stubs.checkVersionAvailability.resolves( false ); + it( 'should throw an error when the version is already in use', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { version: '1.0.0', name: 'stub-package' } ); + vi.mocked( checkVersionAvailability ).mockResolvedValue( false ); - try { - await updateVersions( { version: '1.0.1' } ); - throw new Error( 'Expected to throw.' ); - } catch ( err ) { - expect( err.message ).to.equal( 'The "stub-package@1.0.1" already exists in the npm registry.' ); - } - } ); + await expect( updateVersions( { version: '1.0.1' } ) ) + .rejects.toThrow( 'The "stub-package@1.0.1" already exists in the npm registry.' ); + } ); - it( 'should not throw an error when version is not in use', async () => { - stubs.readJson.resolves( { version: '1.0.0', name: 'stub-package' } ); - stubs.checkVersionAvailability.resolves( true ); + it( 'should not throw an error when version is not in use', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { version: '1.0.0', name: 'stub-package' } ); + vi.mocked( checkVersionAvailability ).mockResolvedValue( true ); - try { - await updateVersions( { version: '1.0.1' } ); - } catch ( err ) { - throw new Error( 'Expected not to throw.' ); - } - } ); + await expect( updateVersions( { version: '1.0.1' } ) ).resolves.toBeNil(); + } ); - it( 'should throw an error when it was not possible to check the version availability', async () => { - stubs.readJson.resolves( { version: '1.0.0', name: 'stub-package' } ); - stubs.checkVersionAvailability.rejects( new Error( 'Custom error.' ) ); + it( 'should throw an error when it was not possible to check the version availability', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { version: '1.0.0', name: 'stub-package' } ); + vi.mocked( checkVersionAvailability ).mockRejectedValue( new Error( 'Custom error.' ) ); - try { - await updateVersions( { version: '1.0.1' } ); - throw new Error( 'Expected to throw.' ); - } catch ( err ) { - expect( err.message ).to.equal( 'Custom error.' ); - } - } ); + await expect( updateVersions( { version: '1.0.1' } ) ) + .rejects.toThrow( 'Custom error.' ); + } ); - it( 'should not use the root package name when checking version availability if `packagesDirectory` is provided', async () => { - stubs.glob.resolves( [ - '/ckeditor5-dev/packages/package1/package.json', - '/ckeditor5-dev/packages/package2/package.json', - '/ckeditor5-dev/package.json' - ] ); - stubs.readJson.withArgs( '/ckeditor5-dev/packages/package1/package.json' ).resolves( { name: 'package1' } ); - stubs.readJson.withArgs( '/ckeditor5-dev/packages/package2/package.json' ).resolves( { name: 'package2' } ); - stubs.readJson.withArgs( '/ckeditor5-dev/package.json' ).resolves( { name: 'root-package' } ); + it( 'should not use the root package name when checking version availability if `packagesDirectory` is provided', async () => { + vi.mocked( glob ).mockResolvedValue( [ + '/ckeditor5-dev/packages/package1/package.json', + '/ckeditor5-dev/packages/package2/package.json', + '/ckeditor5-dev/package.json' + ] ); - await updateVersions( { version: '1.0.1', packagesDirectory: 'packages' } ); + vi.mocked( fs ).readJson.mockImplementation( input => { + if ( input === '/ckeditor5-dev/packages/package1/package.json' ) { + return Promise.resolve( { name: 'package1', version: '1.0.0' } ); + } + + if ( input === '/ckeditor5-dev/packages/package2/package.json' ) { + return Promise.resolve( { name: 'package2', version: '1.0.0' } ); + } - expect( stubs.checkVersionAvailability.callCount ).to.equal( 1 ); - expect( stubs.checkVersionAvailability.firstCall.args[ 1 ] ).to.not.equal( 'root-package' ); + return Promise.resolve( { name: 'root-package', version: '1.0.0' } ); } ); - it( 'should use the root package name when checking version availability if `packagesDirectory` is not provided', async () => { - stubs.glob.resolves( [ '/ckeditor5-dev/package.json' ] ); - stubs.readJson.withArgs( '/ckeditor5-dev/package.json' ).resolves( { name: 'root-package' } ); + await updateVersions( { version: '1.0.1', packagesDirectory: 'packages' } ); - await updateVersions( { version: '1.0.1' } ); + expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledExactlyOnceWith( + '1.0.1', + expect.not.stringContaining( 'root-package' ) + ); + } ); - expect( stubs.checkVersionAvailability.callCount ).to.equal( 1 ); - expect( stubs.checkVersionAvailability.firstCall.args[ 1 ] ).to.equal( 'root-package' ); - } ); + it( 'should use the root package name when checking version availability if `packagesDirectory` is not provided', async () => { + vi.mocked( glob ).mockResolvedValue( [ '/ckeditor5-dev/package.json' ] ); + vi.mocked( fs ).readJson.mockResolvedValue( { name: 'root-package', version: '1.0.0' } ); - it( 'should accept `0.0.0-nightly*` version for nightly releases', async () => { - stubs.readJson.resolves( { version: '1.0.0', name: 'stub-package' } ); + await updateVersions( { version: '1.0.1' } ); - try { - await updateVersions( { version: '0.0.0-nightly-20230510.0' } ); - } catch ( err ) { - throw new Error( 'Expected not to throw.' ); - } - } ); + expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledExactlyOnceWith( '1.0.1', 'root-package' ); + } ); - it( 'should throw when new version is not greater than the current one', async () => { - stubs.readJson.resolves( { version: '1.0.1' } ); + it( 'should accept `0.0.0-nightly*` version for nightly releases', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { version: '1.0.0', name: 'stub-package' } ); - try { - await updateVersions( { version: '1.0.0' } ); - throw new Error( 'Expected to throw.' ); - } catch ( err ) { - expect( err.message ).to.equal( 'Provided version 1.0.0 must be greater than 1.0.1 or match pattern 0.0.0-nightly.' ); - } - } ); + await expect( updateVersions( { version: '0.0.0-nightly-20230510.0' } ) ).resolves.toBeNil(); + } ); - it( 'should throw an error when new version is not a valid semver version', async () => { - try { - await updateVersions( { version: 'x.y.z' } ); - throw new Error( 'Expected to throw.' ); - } catch ( err ) { - expect( err.message ).to.equal( 'Invalid Version: x.y.z' ); - } - } ); + it( 'should throw when new version is not greater than the current one', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { version: '1.0.1' } ); - it( 'should be able to provide custom cwd', async () => { - await updateVersions( { version: '1.0.1', cwd: 'Users/username/ckeditor5-dev/custom-dir' } ); + await expect( updateVersions( { version: '1.0.0' } ) ) + .rejects.toThrow( 'Provided version 1.0.0 must be greater than 1.0.1 or match pattern 0.0.0-nightly.' ); + } ); - expect( stubs.glob.firstCall.args[ 1 ] ).to.deep.equal( { - cwd: 'Users/username/ckeditor5-dev/custom-dir', - absolute: true, - nodir: true - } ); - } ); + it( 'should throw an error when new version is not a valid semver version', async () => { + await expect( updateVersions( { version: 'x.y.z' } ) ) + .rejects.toThrow( 'Invalid Version: x.y.z' ); + } ); + + it( 'should be able to provide custom cwd', async () => { + await updateVersions( { version: '1.0.1', cwd: 'Users/username/ckeditor5-dev/custom-dir' } ); + + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + expect.any( Array ), + expect.objectContaining( { + cwd: 'Users/username/ckeditor5-dev/custom-dir' + } ) + ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/verifypackagespublishedcorrectly.js b/packages/ckeditor5-dev-release-tools/tests/tasks/verifypackagespublishedcorrectly.js index c2dfb8ec5..afbf6c14b 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/verifypackagespublishedcorrectly.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/verifypackagespublishedcorrectly.js @@ -5,141 +5,91 @@ 'use strict'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); - -describe( 'dev-release-tools/utils', () => { - describe( 'verifyPackagesPublishedCorrectly()', () => { - let verifyPackagesPublishedCorrectly, sandbox, stubs; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - fs: { - remove: sandbox.stub().resolves(), - readJson: sandbox.stub().resolves() - }, - devUtils: { - checkVersionAvailability: sandbox.stub().resolves() - }, - glob: { - glob: sandbox.stub().resolves( [] ) - } - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'fs-extra', stubs.fs ); - mockery.registerMock( '../utils/checkversionavailability', stubs.devUtils ); - mockery.registerMock( 'glob', stubs.glob ); - - verifyPackagesPublishedCorrectly = require( '../../lib/tasks/verifypackagespublishedcorrectly' ); - } ); - - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); - } ); - - it( 'should not verify packages if there are no packages in the release directory', async () => { - stubs.glob.glob.resolves( [] ); - - const packagesDirectory = '/workspace/ckeditor5/release/npm'; - const version = 'latest'; - const onSuccess = sandbox.stub(); - - await verifyPackagesPublishedCorrectly( { packagesDirectory, version, onSuccess } ); - - expect( onSuccess.firstCall.args[ 0 ] ).to.equal( 'No packages found to check for upload error 409.' ); - expect( stubs.devUtils.checkVersionAvailability.callCount ).to.equal( 0 ); - } ); - - it( 'should verify packages and remove them from the release directory on "npm show" command success', async () => { - stubs.glob.glob.resolves( [ 'package1', 'package2' ] ); - stubs.fs.readJson - .onCall( 0 ).resolves( { name: '@namespace/package1' } ) - .onCall( 1 ).resolves( { name: '@namespace/package2' } ); - - const packagesDirectory = '/workspace/ckeditor5/release/npm'; - const version = 'latest'; - const onSuccess = sandbox.stub(); - - await verifyPackagesPublishedCorrectly( { packagesDirectory, version, onSuccess } ); - - expect( stubs.devUtils.checkVersionAvailability.firstCall.args[ 0 ] ).to.equal( 'latest' ); - expect( stubs.devUtils.checkVersionAvailability.firstCall.args[ 1 ] ).to.equal( '@namespace/package1' ); - expect( stubs.fs.remove.firstCall.args[ 0 ] ).to.equal( 'package1' ); - - expect( stubs.devUtils.checkVersionAvailability.secondCall.args[ 0 ] ).to.equal( 'latest' ); - expect( stubs.devUtils.checkVersionAvailability.secondCall.args[ 1 ] ).to.equal( '@namespace/package2' ); - expect( stubs.fs.remove.secondCall.args[ 0 ] ).to.equal( 'package2' ); - - expect( onSuccess.firstCall.args[ 0 ] ).to.equal( 'All packages that returned 409 were uploaded correctly.' ); - } ); - - it( 'should not remove package from release directory when package is not available on npm', async () => { - stubs.glob.glob.resolves( [ 'package1', 'package2' ] ); - stubs.fs.readJson - .onCall( 0 ).resolves( { name: '@namespace/package1' } ) - .onCall( 1 ).resolves( { name: '@namespace/package2' } ); - stubs.devUtils.checkVersionAvailability - .onCall( 0 ).resolves( true ) - .onCall( 1 ).resolves( false ); - - const packagesDirectory = '/workspace/ckeditor5/release/npm'; - const version = 'latest'; - const onSuccess = sandbox.stub(); - - await verifyPackagesPublishedCorrectly( { packagesDirectory, version, onSuccess } ) - .then( - () => { - throw new Error( 'this should not be thrown!' ); - }, - e => { - expect( e.message ).to.equal( - 'Packages that were uploaded incorrectly, and need manual verification:\n@namespace/package1' - ); - } - ); - - expect( stubs.fs.remove.callCount ).to.equal( 1 ); - expect( stubs.fs.remove.firstCall.args[ 0 ] ).to.equal( 'package2' ); - } ); - - it( 'should not remove package from release directory when checking version on npm throws error', async () => { - stubs.glob.glob.resolves( [ 'package1', 'package2' ] ); - stubs.fs.readJson - .onCall( 0 ).resolves( { name: '@namespace/package1' } ) - .onCall( 1 ).resolves( { name: '@namespace/package2' } ); - stubs.devUtils.checkVersionAvailability - .onCall( 0 ).rejects() - .onCall( 1 ).resolves(); - - const packagesDirectory = '/workspace/ckeditor5/release/npm'; - const version = 'latest'; - const onSuccess = sandbox.stub(); - - await verifyPackagesPublishedCorrectly( { packagesDirectory, version, onSuccess } ) - .then( - () => { - throw new Error( 'this should not be thrown!' ); - }, - e => { - expect( e.message ).to.equal( - 'Packages that were uploaded incorrectly, and need manual verification:\n@namespace/package1' - ); - } - ); - - expect( stubs.fs.remove.callCount ).to.equal( 1 ); - expect( stubs.fs.remove.firstCall.args[ 0 ] ).to.equal( 'package2' ); - } ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { glob } from 'glob'; +import fs from 'fs-extra'; +import verifyPackagesPublishedCorrectly from '../../lib/tasks/verifypackagespublishedcorrectly.js'; +import checkVersionAvailability from '../../lib/utils/checkversionavailability.js'; + +vi.mock( 'fs-extra' ); +vi.mock( '../../lib/utils/checkversionavailability' ); +vi.mock( 'glob' ); + +describe( 'verifyPackagesPublishedCorrectly()', () => { + beforeEach( () => { + vi.mocked( fs ).remove.mockResolvedValue(); + vi.mocked( fs ).readJson.mockResolvedValue(); + vi.mocked( glob ).mockResolvedValue( [] ); + vi.mocked( checkVersionAvailability ).mockResolvedValue(); + } ); + + it( 'should not verify packages if there are no packages in the release directory', async () => { + const packagesDirectory = '/workspace/ckeditor5/release/npm'; + const version = 'latest'; + const onSuccess = vi.fn(); + + await verifyPackagesPublishedCorrectly( { packagesDirectory, version, onSuccess } ); + + expect( onSuccess ).toHaveBeenCalledExactlyOnceWith( 'No packages found to check for upload error 409.' ); + expect( vi.mocked( checkVersionAvailability ) ).not.toHaveBeenCalled(); + } ); + + it( 'should verify packages and remove them from the release directory on "npm show" command success', async () => { + vi.mocked( glob ).mockResolvedValue( [ 'package1', 'package2' ] ); + vi.mocked( fs ).readJson + .mockResolvedValueOnce( { name: '@namespace/package1' } ) + .mockResolvedValueOnce( { name: '@namespace/package2' } ); + + const packagesDirectory = '/workspace/ckeditor5/release/npm'; + const version = 'latest'; + const onSuccess = vi.fn(); + + await verifyPackagesPublishedCorrectly( { packagesDirectory, version, onSuccess } ); + + expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledWith( 'latest', '@namespace/package1' ); + expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledWith( 'latest', '@namespace/package2' ); + expect( vi.mocked( fs ).remove ).toHaveBeenCalledWith( 'package1' ); + expect( vi.mocked( fs ).remove ).toHaveBeenCalledWith( 'package2' ); + + expect( onSuccess ).toHaveBeenCalledExactlyOnceWith( 'All packages that returned 409 were uploaded correctly.' ); + } ); + + it( 'should not remove package from release directory when package is not available on npm', async () => { + vi.mocked( glob ).mockResolvedValue( [ 'package1', 'package2' ] ); + vi.mocked( fs ).readJson + .mockResolvedValueOnce( { name: '@namespace/package1' } ) + .mockResolvedValueOnce( { name: '@namespace/package2' } ); + vi.mocked( checkVersionAvailability ) + .mockResolvedValueOnce( true ) + .mockResolvedValueOnce( false ); + + const packagesDirectory = '/workspace/ckeditor5/release/npm'; + const version = 'latest'; + const onSuccess = vi.fn(); + + await expect( verifyPackagesPublishedCorrectly( { packagesDirectory, version, onSuccess } ) ) + .rejects.toThrow( 'Packages that were uploaded incorrectly, and need manual verification:\n@namespace/package1' ); + + expect( vi.mocked( fs ).remove ).toHaveBeenCalledExactlyOnceWith( 'package2' ); + } ); + + it( 'should not remove package from release directory when checking version on npm throws error', async () => { + vi.mocked( glob ).mockResolvedValue( [ 'package1', 'package2' ] ); + vi.mocked( fs ).readJson + .mockResolvedValueOnce( { name: '@namespace/package1' } ) + .mockResolvedValueOnce( { name: '@namespace/package2' } ); + vi.mocked( checkVersionAvailability ) + .mockRejectedValueOnce( ) + .mockResolvedValueOnce( false ); + + const packagesDirectory = '/workspace/ckeditor5/release/npm'; + const version = 'latest'; + const onSuccess = vi.fn(); + + await expect( verifyPackagesPublishedCorrectly( { packagesDirectory, version, onSuccess } ) ) + .rejects + .toThrow( 'Packages that were uploaded incorrectly, and need manual verification:\n@namespace/package1' ); + + expect( vi.mocked( fs ).remove ).toHaveBeenCalledExactlyOnceWith( 'package2' ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/templates/commit.js b/packages/ckeditor5-dev-release-tools/tests/templates/commit.js index 1fac7ac82..b02b23bf0 100644 --- a/packages/ckeditor5-dev-release-tools/tests/templates/commit.js +++ b/packages/ckeditor5-dev-release-tools/tests/templates/commit.js @@ -3,12 +3,14 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const handlebars = require( 'handlebars' ); -const expect = require( 'chai' ).expect; +import { beforeEach, describe, expect, it } from 'vitest'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; +import path from 'upath'; +import handlebars from 'handlebars'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); const templatePath = path.resolve( __dirname, '..', '..', 'lib', 'templates', 'commit.hbs' ); const templateContent = fs.readFileSync( templatePath, 'utf-8' ); diff --git a/packages/ckeditor5-dev-release-tools/tests/test-fixtures/.gitkeep b/packages/ckeditor5-dev-release-tools/tests/test-fixtures/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/abortcontroller.js b/packages/ckeditor5-dev-release-tools/tests/utils/abortcontroller.js index 22aa90efa..dc53e7def 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/abortcontroller.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/abortcontroller.js @@ -3,99 +3,84 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); - -describe( 'dev-release-tools/utils', () => { - describe( 'abortController()', () => { - let abortController, listeners, sandbox, stubs; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - listeners = {}; - - stubs = { - process: { - prependOnceListener: sandbox.stub().callsFake( ( eventName, listener ) => { - listeners[ eventName ] = new Set( [ - listener, - ...listeners[ eventName ] || [] - ] ); - } ), - removeListener: sandbox.stub().callsFake( ( eventName, listener ) => { - if ( listeners[ eventName ] ) { - listeners[ eventName ].delete( listener ); - } - } ) - } - }; - - sandbox.stub( process, 'prependOnceListener' ).callsFake( stubs.process.prependOnceListener ); - sandbox.stub( process, 'removeListener' ).callsFake( stubs.process.removeListener ); - - abortController = require( '../../lib/utils/abortcontroller' ); - } ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { deregisterAbortController, registerAbortController } from '../../lib/utils/abortcontroller.js'; + +vi.stubGlobal( 'process', { + prependOnceListener: vi.fn(), + removeListener: vi.fn() +} ); + +describe( 'abortcontroller', () => { + let listeners; + + beforeEach( () => { + listeners = {}; - afterEach( () => { - sandbox.restore(); + vi.mocked( process ).prependOnceListener.mockImplementation( ( eventName, listener ) => { + listeners[ eventName ] = new Set( [ + listener, + ...listeners[ eventName ] || [] + ] ); } ); + vi.mocked( process ).removeListener.mockImplementation( ( eventName, listener ) => { + if ( listeners[ eventName ] ) { + listeners[ eventName ].delete( listener ); + } + } ); + } ); + describe( 'registerAbortController()', () => { it( 'should return AbortController instance', () => { - const abortControllerInstance = abortController.registerAbortController(); + const abortControllerInstance = registerAbortController(); expect( abortControllerInstance ).to.be.instanceof( global.AbortController ); } ); it( 'should store listener in internal property for further use', () => { - const abortControllerInstance = abortController.registerAbortController(); + const abortControllerInstance = registerAbortController(); expect( abortControllerInstance._listener ).to.be.a( 'function' ); } ); it( 'should register listener on SIGINT event', () => { - const abortControllerInstance = abortController.registerAbortController(); + const abortControllerInstance = registerAbortController(); - expect( stubs.process.prependOnceListener.callCount ).to.equal( 1 ); - expect( stubs.process.prependOnceListener.firstCall.args[ 0 ] ).to.equal( 'SIGINT' ); - expect( stubs.process.prependOnceListener.firstCall.args[ 1 ] ).to.equal( abortControllerInstance._listener ); + expect( + vi.mocked( process ).prependOnceListener + ).toHaveBeenCalledExactlyOnceWith( 'SIGINT', abortControllerInstance._listener ); } ); it( 'should call abort method on SIGINT event', () => { - const abortControllerInstance = abortController.registerAbortController(); + const abortControllerInstance = registerAbortController(); - sandbox.spy( abortControllerInstance, 'abort' ); + vi.spyOn( abortControllerInstance, 'abort' ); listeners.SIGINT.forEach( listener => listener() ); - expect( abortControllerInstance.abort.callCount ).to.equal( 1 ); - expect( abortControllerInstance.abort.firstCall.args[ 0 ] ).to.equal( 'SIGINT' ); + expect( abortControllerInstance.abort ).toHaveBeenCalledExactlyOnceWith( 'SIGINT' ); } ); + } ); + describe( 'deregisterAbortController()', () => { it( 'should not deregister listener if AbortController instance is not set', () => { - abortController.deregisterAbortController(); - - expect( stubs.process.removeListener.callCount ).to.equal( 0 ); + deregisterAbortController(); + expect( vi.mocked( process ).removeListener ).not.toHaveBeenCalled(); } ); it( 'should not deregister listener if AbortController instance is not registered', () => { const abortControllerInstance = new AbortController(); + deregisterAbortController( abortControllerInstance ); - abortController.deregisterAbortController( abortControllerInstance ); - - expect( stubs.process.removeListener.callCount ).to.equal( 0 ); + expect( vi.mocked( process ).removeListener ).not.toHaveBeenCalled(); } ); it( 'should deregister listener if AbortController instance is registered', () => { - const abortControllerInstance = abortController.registerAbortController(); + const abortControllerInstance = registerAbortController(); - abortController.deregisterAbortController( abortControllerInstance ); + deregisterAbortController( abortControllerInstance ); - expect( stubs.process.removeListener.callCount ).to.equal( 1 ); - expect( stubs.process.removeListener.firstCall.args[ 0 ] ).to.equal( 'SIGINT' ); - expect( stubs.process.removeListener.firstCall.args[ 1 ] ).to.equal( abortControllerInstance._listener ); + expect( vi.mocked( process ).removeListener ).toHaveBeenCalledExactlyOnceWith( 'SIGINT', abortControllerInstance._listener ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/assertfilestopublish.js b/packages/ckeditor5-dev-release-tools/tests/utils/assertfilestopublish.js index a9474e58d..1bef66874 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/assertfilestopublish.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/assertfilestopublish.js @@ -3,366 +3,387 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); - -describe( 'dev-release-tools/utils', () => { - describe( 'assertFilesToPublish()', () => { - let assertFilesToPublish, sandbox, stubs; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - fs: { - readJson: sandbox.stub() - }, - glob: { - glob: sandbox.stub() - } - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); +import { describe, expect, it, vi } from 'vitest'; +import fs from 'fs-extra'; +import { glob } from 'glob'; +import assertFilesToPublish from '../../lib/utils/assertfilestopublish.js'; - mockery.registerMock( 'fs-extra', stubs.fs ); - mockery.registerMock( 'glob', stubs.glob ); +vi.mock( 'fs-extra' ); +vi.mock( 'glob' ); - assertFilesToPublish = require( '../../lib/utils/assertfilestopublish' ); - } ); +describe( 'assertFilesToPublish()', () => { + it( 'should do nothing if list of packages is empty', async () => { + await assertFilesToPublish( [] ); + + expect( vi.mocked( fs ).readJson ).not.toHaveBeenCalled(); + expect( vi.mocked( glob ) ).not.toHaveBeenCalled(); + } ); + + it( 'should read `package.json` from each package', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( {} ); + + await assertFilesToPublish( [ 'ckeditor5-foo', 'ckeditor5-bar' ] ); + + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( 'ckeditor5-foo/package.json' ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( 'ckeditor5-bar/package.json' ); + } ); - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); + it( 'should not check any file if `package.json` does not contain required files', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo' } ); - it( 'should do nothing if list of packages is empty', () => { - return assertFilesToPublish( [] ) - .then( () => { - expect( stubs.fs.readJson.called ).to.equal( false ); - expect( stubs.glob.glob.called ).to.equal( false ); - } ); + await assertFilesToPublish( [ 'ckeditor5-foo' ] ); + + expect( vi.mocked( glob ) ).not.toHaveBeenCalled(); + } ); + + it( 'should not throw if all files from `files` field exist', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + files: [ + 'src', + 'README.md' + ] } ); - it( 'should read `package.json` from each package', () => { - stubs.fs.readJson.resolves( {} ); + vi.mocked( glob ).mockImplementation( input => { + if ( input[ 0 ] === 'src' ) { + return Promise.resolve( [ 'src/index.ts' ] ); + } - return assertFilesToPublish( [ 'ckeditor5-foo', 'ckeditor5-bar' ] ) - .then( () => { - expect( stubs.fs.readJson.callCount ).to.equal( 2 ); - expect( stubs.fs.readJson.firstCall.args[ 0 ] ).to.equal( 'ckeditor5-foo/package.json' ); - expect( stubs.fs.readJson.secondCall.args[ 0 ] ).to.equal( 'ckeditor5-bar/package.json' ); - } ); + return Promise.resolve( [ 'README.md' ] ); } ); - it( 'should not check any file if `package.json` does not contain required files', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo' - } ); + await assertFilesToPublish( [ 'ckeditor5-foo' ] ); + + expect( vi.mocked( glob ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'src', 'src/**' ], + expect.objectContaining( { + cwd: 'ckeditor5-foo', + dot: true, + nodir: true + } ) + ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'README.md', 'README.md/**' ], + expect.objectContaining( { + cwd: 'ckeditor5-foo', + dot: true, + nodir: true + } ) + ); + } ); - return assertFilesToPublish( [ 'ckeditor5-foo' ] ) - .then( () => { - expect( stubs.glob.glob.called ).to.equal( false ); - } ); + it( 'should not throw if all files from `files` field exist except the optional ones (for package)', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + files: [ + 'src', + 'README.md' + ] } ); - it( 'should not throw if all files from `files` field exist', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - files: [ - 'src', - 'README.md' - ] - } ); + vi.mocked( glob ).mockImplementation( input => { + if ( input[ 0 ] === 'src' ) { + return Promise.resolve( [ 'src/index.ts' ] ); + } - stubs.glob.glob - .withArgs( [ 'src', 'src/**' ] ).resolves( [ 'src/index.ts' ] ) - .withArgs( [ 'README.md', 'README.md/**' ] ).resolves( [ 'README.md' ] ); - - return assertFilesToPublish( [ 'ckeditor5-foo' ] ) - .then( () => { - expect( stubs.glob.glob.callCount ).to.equal( 2 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'src', 'src/**' ] ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'cwd', 'ckeditor5-foo' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'dot', true ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'nodir', true ); - expect( stubs.glob.glob.secondCall.args[ 0 ] ).to.deep.equal( [ 'README.md', 'README.md/**' ] ); - expect( stubs.glob.glob.secondCall.args[ 1 ] ).to.have.property( 'cwd', 'ckeditor5-foo' ); - expect( stubs.glob.glob.secondCall.args[ 1 ] ).to.have.property( 'dot', true ); - expect( stubs.glob.glob.secondCall.args[ 1 ] ).to.have.property( 'nodir', true ); - } ); + return Promise.resolve( [ 'README.md' ] ); } ); - it( 'should not throw if all files from `files` field exist except the optional ones (for package)', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - files: [ - 'src', - 'README.md' - ] - } ); + const optionalEntries = { + 'ckeditor5-foo': [ + 'README.md' + ] + }; + + await assertFilesToPublish( [ 'ckeditor5-foo' ], optionalEntries ); + + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ 'src', 'src/**' ], + expect.objectContaining( { + cwd: 'ckeditor5-foo', + dot: true, + nodir: true + } ) + ); + } ); - stubs.glob.glob - .withArgs( [ 'src', 'src/**' ] ).resolves( [ 'src/index.ts' ] ) - .withArgs( [ 'README.md', 'README.md/**' ] ).resolves( [ 'README.md' ] ); + it( 'should not throw if all files from `files` field exist except the optional ones (for all packages)', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + files: [ + 'src', + 'README.md' + ] + } ); - const optionalEntries = { - 'ckeditor5-foo': [ - 'README.md' - ] - }; + vi.mocked( glob ).mockImplementation( input => { + if ( input[ 0 ] === 'src' ) { + return Promise.resolve( [ 'src/index.ts' ] ); + } - return assertFilesToPublish( [ 'ckeditor5-foo' ], optionalEntries ) - .then( () => { - expect( stubs.glob.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'src', 'src/**' ] ); - } ); + return Promise.resolve( [] ); } ); - it( 'should not throw if all files from `files` field exist except the optional ones (for all packages)', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - files: [ - 'src', - 'README.md' - ] - } ); + const optionalEntries = { + 'default': [ + 'README.md' + ] + }; + + await assertFilesToPublish( [ 'ckeditor5-foo' ], optionalEntries ); + + expect( vi.mocked( glob ) ).toHaveBeenCalledExactlyOnceWith( + [ 'src', 'src/**' ], + expect.objectContaining( { + cwd: 'ckeditor5-foo', + dot: true, + nodir: true + } ) + ); + } ); - stubs.glob.glob - .withArgs( [ 'src', 'src/**' ] ).resolves( [ 'src/index.ts' ] ) - .withArgs( [ 'README.md', 'README.md/**' ] ).resolves( [] ); + it( 'should prefer own configuration for optional entries', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + files: [ + 'src', + 'README.md' + ] + } ); - const optionalEntries = { - 'default': [ - 'README.md' - ] - }; + vi.mocked( glob ).mockImplementation( input => { + if ( input[ 0 ] === 'src' ) { + return Promise.resolve( [ 'src/index.ts' ] ); + } - return assertFilesToPublish( [ 'ckeditor5-foo' ], optionalEntries ) - .then( () => { - expect( stubs.glob.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'src', 'src/**' ] ); - } ); + return Promise.resolve( [ 'README.md' ] ); } ); - it( 'should prefer own configuration for optional entries', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - files: [ - 'src', - 'README.md' - ] - } ); + const optionalEntries = { + // Make all entries as required for the "ckeditor5-foo" package. + 'ckeditor5-foo': [], + 'default': [ + 'README.md' + ] + }; + + await assertFilesToPublish( [ 'ckeditor5-foo' ], optionalEntries ); + + expect( vi.mocked( glob ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'src', 'src/**' ], + expect.objectContaining( { + cwd: 'ckeditor5-foo', + dot: true, + nodir: true + } ) + ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'README.md', 'README.md/**' ], + expect.objectContaining( { + cwd: 'ckeditor5-foo', + dot: true, + nodir: true + } ) + ); + } ); - stubs.glob.glob - .withArgs( [ 'src', 'src/**' ] ).resolves( [ 'src/index.ts' ] ) - .withArgs( [ 'README.md', 'README.md/**' ] ).resolves( [ 'README.md' ] ); + it( 'should consider entry as required if there are not matches in optional entries', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + files: [ + 'src', + 'README.md' + ] + } ); - const optionalEntries = { - // Make all entries as required for the "ckeditor5-foo" package. - 'ckeditor5-foo': [], - 'default': [ - 'README.md' - ] - }; + vi.mocked( glob ).mockImplementation( input => { + if ( input[ 0 ] === 'src' ) { + return Promise.resolve( [ 'src/index.ts' ] ); + } - return assertFilesToPublish( [ 'ckeditor5-foo' ], optionalEntries ) - .then( () => { - expect( stubs.glob.glob.callCount ).to.equal( 2 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'src', 'src/**' ] ); - expect( stubs.glob.glob.secondCall.args[ 0 ] ).to.deep.equal( [ 'README.md', 'README.md/**' ] ); - } ); + return Promise.resolve( [ 'README.md' ] ); } ); - it( 'should consider entry as required if there are not matches in optional entries', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - files: [ - 'src', - 'README.md' - ] - } ); + const optionalEntries = { + 'ckeditor5-bar': [ + 'src', + 'README.md' + ] + }; + + await assertFilesToPublish( [ 'ckeditor5-foo' ], optionalEntries ); + + expect( vi.mocked( glob ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'src', 'src/**' ], + expect.any( Object ) + ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'README.md', 'README.md/**' ], + expect.any( Object ) + ); + } ); - stubs.glob.glob - .withArgs( [ 'src', 'src/**' ] ).resolves( [ 'src/index.ts' ] ) - .withArgs( [ 'README.md', 'README.md/**' ] ).resolves( [ 'README.md' ] ); + it( 'should not throw if `main` file exists', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + main: 'src/index.ts', + files: [ + 'src', + 'README.md' + ] + } ); - const optionalEntries = { - 'ckeditor5-bar': [ - 'src', - 'README.md' - ] - }; + vi.mocked( glob ).mockImplementation( input => { + if ( input[ 0 ] === 'src' ) { + return Promise.resolve( [ 'src/index.ts' ] ); + } + if ( input[ 0 ] === 'src/index.ts' ) { + return Promise.resolve( [ 'src/index.ts' ] ); + } - return assertFilesToPublish( [ 'ckeditor5-foo' ], optionalEntries ) - .then( () => { - expect( stubs.glob.glob.callCount ).to.equal( 2 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'src', 'src/**' ] ); - expect( stubs.glob.glob.secondCall.args[ 0 ] ).to.deep.equal( [ 'README.md', 'README.md/**' ] ); - } ); + return Promise.resolve( [ 'README.md' ] ); } ); - it( 'should not throw if `main` file exists', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - main: 'src/index.ts', - files: [ - 'src', - 'README.md' - ] - } ); + await assertFilesToPublish( [ 'ckeditor5-foo' ] ); + + expect( vi.mocked( glob ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'src', 'src/**' ], + expect.any( Object ) + ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'src/index.ts', 'src/index.ts/**' ], + expect.any( Object ) + ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'README.md', 'README.md/**' ], + expect.any( Object ) + ); + } ); - stubs.glob.glob - .withArgs( [ 'src', 'src/**' ] ).resolves( [ 'src/index.ts' ] ) - .withArgs( [ 'src/index.ts', 'src/index.ts/**' ] ).resolves( [ 'src/index.ts' ] ) - .withArgs( [ 'README.md', 'README.md/**' ] ).resolves( [ 'README.md' ] ); - - return assertFilesToPublish( [ 'ckeditor5-foo' ] ) - .then( () => { - expect( stubs.glob.glob.callCount ).to.equal( 3 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'src/index.ts', 'src/index.ts/**' ] ); - expect( stubs.glob.glob.secondCall.args[ 0 ] ).to.deep.equal( [ 'src', 'src/**' ] ); - expect( stubs.glob.glob.thirdCall.args[ 0 ] ).to.deep.equal( [ 'README.md', 'README.md/**' ] ); - } ); + it( 'should not throw if `types` file exists', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + types: 'src/index.d.ts', + files: [ + 'src', + 'README.md' + ] } ); - it( 'should not throw if `types` file exists', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - types: 'src/index.d.ts', - files: [ - 'src', - 'README.md' - ] - } ); + vi.mocked( glob ).mockImplementation( input => { + if ( input[ 0 ] === 'src' ) { + return Promise.resolve( [ 'src/index.ts' ] ); + } + if ( input[ 0 ] === 'src/index.d.ts' ) { + return Promise.resolve( [ 'src/index.d.ts' ] ); + } - stubs.glob.glob - .withArgs( [ 'src', 'src/**' ] ).resolves( [ 'src/index.ts' ] ) - .withArgs( [ 'src/index.d.ts', 'src/index.d.ts/**' ] ).resolves( [ 'src/index.d.ts' ] ) - .withArgs( [ 'README.md', 'README.md/**' ] ).resolves( [ 'README.md' ] ); - - return assertFilesToPublish( [ 'ckeditor5-foo' ] ) - .then( () => { - expect( stubs.glob.glob.callCount ).to.equal( 3 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.deep.equal( [ 'src/index.d.ts', 'src/index.d.ts/**' ] ); - expect( stubs.glob.glob.secondCall.args[ 0 ] ).to.deep.equal( [ 'src', 'src/**' ] ); - expect( stubs.glob.glob.thirdCall.args[ 0 ] ).to.deep.equal( [ 'README.md', 'README.md/**' ] ); - } ); + return Promise.resolve( [ 'README.md' ] ); } ); - it( 'should throw if not all files from `files` field exist', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - files: [ - 'src', - 'LICENSE.md', - 'README.md' - ] - } ); + await assertFilesToPublish( [ 'ckeditor5-foo' ] ); + + expect( vi.mocked( glob ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'src', 'src/**' ], + expect.any( Object ) + ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'src/index.d.ts', 'src/index.d.ts/**' ], + expect.any( Object ) + ); + expect( vi.mocked( glob ) ).toHaveBeenCalledWith( + [ 'README.md', 'README.md/**' ], + expect.any( Object ) + ); + } ); - stubs.glob.glob.resolves( [] ); - - return assertFilesToPublish( [ 'ckeditor5-foo' ] ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'Missing files in "ckeditor5-foo" package for entries: "src", "LICENSE.md", "README.md"' - ); - } ); + it( 'should throw if not all files from `files` field exist', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + files: [ + 'src', + 'LICENSE.md', + 'README.md' + ] } ); - it( 'should throw if file from `main` field does not exist', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - main: 'src/index.ts' - } ); + vi.mocked( glob ).mockResolvedValue( [] ); - stubs.glob.glob.resolves( [] ); - - return assertFilesToPublish( [ 'ckeditor5-foo' ] ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'Missing files in "ckeditor5-foo" package for entries: "src/index.ts"' - ); - } ); + await expect( assertFilesToPublish( [ 'ckeditor5-foo' ] ) ) + .rejects.toThrow( 'Missing files in "ckeditor5-foo" package for entries: "src", "LICENSE.md", "README.md"' ); + } ); + + it( 'should throw if file from `main` field does not exist', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + main: 'src/index.ts' } ); - it( 'should throw if file from `types` field does not exist', () => { - stubs.fs.readJson.resolves( { - name: 'ckeditor5-foo', - types: 'src/index.d.ts' - } ); + vi.mocked( glob ).mockResolvedValue( [] ); - stubs.glob.glob.resolves( [] ); - - return assertFilesToPublish( [ 'ckeditor5-foo' ] ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'Missing files in "ckeditor5-foo" package for entries: "src/index.d.ts"' - ); - } ); + await expect( assertFilesToPublish( [ 'ckeditor5-foo' ] ) ) + .rejects.toThrow( 'Missing files in "ckeditor5-foo" package for entries: "src/index.ts"' ); + } ); + + it( 'should throw if file from `types` field does not exist', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + types: 'src/index.d.ts' } ); - it( 'should throw one error for all packages with missing files', () => { - stubs.fs.readJson - .withArgs( 'ckeditor5-foo/package.json' ).resolves( { + vi.mocked( glob ).mockResolvedValue( [] ); + + await expect( assertFilesToPublish( [ 'ckeditor5-foo' ] ) ) + .rejects.toThrow( 'Missing files in "ckeditor5-foo" package for entries: "src/index.d.ts"' ); + } ); + + it( 'should throw one error for all packages with missing files', async () => { + vi.mocked( fs ).readJson.mockImplementation( input => { + if ( input === 'ckeditor5-foo/package.json' ) { + return Promise.resolve( { name: 'ckeditor5-foo', files: [ 'src' ] - } ) - .withArgs( 'ckeditor5-bar/package.json' ).resolves( { + } ); + } + + if ( input === 'ckeditor5-bar/package.json' ) { + return Promise.resolve( { name: 'ckeditor5-bar', files: [ 'src', 'README.md' ] - } ) - .withArgs( 'ckeditor5-baz/package.json' ).resolves( { - name: 'ckeditor5-baz', - files: [ - 'src', - 'LICENSE.md', - 'README.md' - ] } ); + } - stubs.glob.glob.resolves( [] ); - - return assertFilesToPublish( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ] ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'Missing files in "ckeditor5-foo" package for entries: "src"\n' + - 'Missing files in "ckeditor5-bar" package for entries: "src", "README.md"\n' + - 'Missing files in "ckeditor5-baz" package for entries: "src", "LICENSE.md", "README.md"' - ); - } ); + return Promise.resolve( { + name: 'ckeditor5-baz', + files: [ + 'src', + 'LICENSE.md', + 'README.md' + ] + } ); } ); + + vi.mocked( glob ).mockResolvedValue( [] ); + + const errorMessage = 'Missing files in "ckeditor5-foo" package for entries: "src"\n' + + 'Missing files in "ckeditor5-bar" package for entries: "src", "README.md"\n' + + 'Missing files in "ckeditor5-baz" package for entries: "src", "LICENSE.md", "README.md"'; + + await expect( assertFilesToPublish( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ] ) ) + .rejects.toThrow( errorMessage ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/assertnpmauthorization.js b/packages/ckeditor5-dev-release-tools/tests/utils/assertnpmauthorization.js index 5d5bb931e..fe75a5ac8 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/assertnpmauthorization.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/assertnpmauthorization.js @@ -3,92 +3,45 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi } from 'vitest'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import assertNpmAuthorization from '../../lib/utils/assertnpmauthorization.js'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); -describe( 'dev-release-tools/utils', () => { - describe( 'assertNpmAuthorization()', () => { - let assertNpmAuthorization, sandbox, stubs; +describe( 'assertNpmAuthorization()', () => { + it( 'should not throw if user is logged to npm as the provided account name', async () => { + vi.mocked( tools ).shExec.mockResolvedValue( 'pepe' ); - beforeEach( () => { - sandbox = sinon.createSandbox(); + await assertNpmAuthorization( 'pepe' ); - stubs = { - devUtils: { - tools: { - shExec: sandbox.stub().resolves() - } - } - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', stubs.devUtils ); - - assertNpmAuthorization = require( '../../lib/utils/assertnpmauthorization' ); - } ); - - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); - } ); - - it( 'should not throw if user is logged to npm as the provided account name', () => { - stubs.devUtils.tools.shExec.resolves( 'pepe' ); - - return assertNpmAuthorization( 'pepe' ) - .then( () => { - expect( stubs.devUtils.tools.shExec.callCount ).to.equal( 1 ); - expect( stubs.devUtils.tools.shExec.firstCall.args[ 0 ] ).to.equal( 'npm whoami' ); - expect( stubs.devUtils.tools.shExec.firstCall.args[ 1 ] ).to.have.property( 'verbosity', 'error' ); - expect( stubs.devUtils.tools.shExec.firstCall.args[ 1 ] ).to.have.property( 'async', true ); - } ); - } ); + expect( vi.mocked( tools ).shExec ).toHaveBeenCalledExactlyOnceWith( + 'npm whoami', + expect.objectContaining( { + verbosity: 'error', + async: true + } ) + ); + } ); - it( 'should trim whitespace characters from the command output before checking the name', () => { - stubs.devUtils.tools.shExec.resolves( '\t pepe \n' ); + it( 'should trim whitespace characters from the command output before checking the name', async () => { + vi.mocked( tools ).shExec.mockResolvedValue( '\t pepe \n' ); - return assertNpmAuthorization( 'pepe' ); - } ); + await assertNpmAuthorization( 'pepe' ); + } ); - it( 'should throw if user is not logged to npm', () => { - stubs.devUtils.tools.shExec.rejects(); + it( 'should throw if user is not logged to npm', async () => { + vi.mocked( tools ).shExec.mockRejectedValue( new Error( 'not logged' ) ); - return assertNpmAuthorization( 'pepe' ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'You must be logged to npm as "pepe" to execute this release step.' - ); - } ); - } ); + await expect( assertNpmAuthorization( 'pepe' ) ) + .rejects.toThrow( 'You must be logged to npm as "pepe" to execute this release step.' ); + } ); - it( 'should throw if user is logged to npm on different account name', () => { - stubs.devUtils.tools.shExec.resolves( 'john' ); + it( 'should throw if user is logged to npm on different account name', async () => { + vi.mocked( tools ).shExec.mockResolvedValue( 'john' ); - return assertNpmAuthorization( 'pepe' ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'You must be logged to npm as "pepe" to execute this release step.' - ); - } ); - } ); + await expect( assertNpmAuthorization( 'pepe' ) ) + .rejects.toThrow( 'You must be logged to npm as "pepe" to execute this release step.' ); } ); -} ); +} ) +; diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/assertnpmtag.js b/packages/ckeditor5-dev-release-tools/tests/utils/assertnpmtag.js index 90c3cacb0..716ca19e1 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/assertnpmtag.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/assertnpmtag.js @@ -3,205 +3,142 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi } from 'vitest'; +import fs from 'fs-extra'; +import assertNpmTag from '../../lib/utils/assertnpmtag.js'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +vi.mock( 'fs-extra' ); -describe( 'dev-release-tools/utils', () => { - describe( 'assertNpmTag()', () => { - let assertNpmTag, sandbox, stubs; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - fs: { - readJson: sandbox.stub() - } - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'fs-extra', stubs.fs ); - - assertNpmTag = require( '../../lib/utils/assertnpmtag' ); - } ); - - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); - } ); - - it( 'should resolve the promise if list of packages is empty', () => { - return assertNpmTag( [] ); - } ); +describe( 'assertNpmTag()', () => { + it( 'should resolve the promise if list of packages is empty', async () => { + await assertNpmTag( [] ); + } ); - it( 'should read `package.json` from each package', () => { - stubs.fs.readJson - .withArgs( 'ckeditor5-foo/package.json' ).resolves( { + it( 'should read `package.json` from each package', async () => { + vi.mocked( fs ).readJson.mockImplementation( input => { + if ( input === 'ckeditor5-foo/package.json' ) { + return Promise.resolve( { name: 'ckeditor5-foo', version: '1.0.0' - } ) - .withArgs( 'ckeditor5-bar/package.json' ).resolves( { - name: 'ckeditor5-bar', - version: '0.0.1' } ); + } - return assertNpmTag( [ 'ckeditor5-foo', 'ckeditor5-bar' ], 'latest' ) - .then( () => { - expect( stubs.fs.readJson.callCount ).to.equal( 2 ); - expect( stubs.fs.readJson.firstCall.args[ 0 ] ).to.equal( 'ckeditor5-foo/package.json' ); - expect( stubs.fs.readJson.secondCall.args[ 0 ] ).to.equal( 'ckeditor5-bar/package.json' ); - } ); + return Promise.resolve( { + name: 'ckeditor5-bar', + version: '0.0.1' + } ); } ); - it( 'should not throw if version tag matches npm tag (both "latest")', () => { - stubs.fs.readJson.withArgs( 'ckeditor5-foo/package.json' ).resolves( { - name: 'ckeditor5-foo', - version: '1.0.0' - } ); + await assertNpmTag( [ 'ckeditor5-foo', 'ckeditor5-bar' ], 'latest' ); + + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( 'ckeditor5-foo/package.json' ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( 'ckeditor5-bar/package.json' ); + } ); - return assertNpmTag( [ 'ckeditor5-foo' ], 'latest' ); + it( 'should not throw if version tag matches npm tag (both "latest")', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + version: '1.0.0' } ); - it( 'should not throw if version tag matches npm tag (version tag = "latest", npm tag = "staging")', () => { - stubs.fs.readJson.withArgs( 'ckeditor5-foo/package.json' ).resolves( { - name: 'ckeditor5-foo', - version: '1.0.0' - } ); + await assertNpmTag( [ 'ckeditor5-foo' ], 'latest' ); + } ); - return assertNpmTag( [ 'ckeditor5-foo' ], 'staging' ); + it( 'should not throw if version tag matches npm tag (version tag = "latest", npm tag = "staging")', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + version: '1.0.0' } ); - it( 'should not throw if version tag matches npm tag (both "alpha")', () => { - stubs.fs.readJson.withArgs( 'ckeditor5-foo/package.json' ).resolves( { - name: 'ckeditor5-foo', - version: '1.0.0-alpha.0' - } ); + await assertNpmTag( [ 'ckeditor5-foo' ], 'staging' ); + } ); - return assertNpmTag( [ 'ckeditor5-foo' ], 'alpha' ); + it( 'should not throw if version tag matches npm tag (both "alpha")', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + version: '1.0.0-alpha.0' } ); - it( 'should not throw if version tag matches npm tag (both "nightly")', () => { - stubs.fs.readJson.withArgs( 'ckeditor5-foo/package.json' ).resolves( { - name: 'ckeditor5-foo', - version: '0.0.0-nightly-20230517.0' - } ); + await assertNpmTag( [ 'ckeditor5-foo' ], 'alpha' ); + } ); - return assertNpmTag( [ 'ckeditor5-foo' ], 'nightly' ); + it( 'should not throw if version tag matches npm tag (both "nightly")', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + version: '0.0.0-nightly-20230517.0' } ); - it( 'should throw if version tag does not match npm tag (version tag = "latest", npm tag = "alpha")', () => { - stubs.fs.readJson.withArgs( 'ckeditor5-foo/package.json' ).resolves( { - name: 'ckeditor5-foo', - version: '1.0.0' - } ); + await assertNpmTag( [ 'ckeditor5-foo' ], 'nightly' ); + } ); - return assertNpmTag( [ 'ckeditor5-foo' ], 'alpha' ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'The version tag "latest" from "ckeditor5-foo" package does not match the npm tag "alpha".' - ); - } ); + it( 'should throw if version tag does not match npm tag (version tag = "latest", npm tag = "alpha")', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + version: '1.0.0' } ); - it( 'should throw if version tag does not match npm tag (version tag = "latest", npm tag = "nightly")', () => { - stubs.fs.readJson.withArgs( 'ckeditor5-foo/package.json' ).resolves( { - name: 'ckeditor5-foo', - version: '1.0.0' - } ); + await expect( assertNpmTag( [ 'ckeditor5-foo' ], 'alpha' ) ) + .rejects.toThrow( 'The version tag "latest" from "ckeditor5-foo" package does not match the npm tag "alpha".' ); + } ); - return assertNpmTag( [ 'ckeditor5-foo' ], 'nightly' ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'The version tag "latest" from "ckeditor5-foo" package does not match the npm tag "nightly".' - ); - } ); + it( 'should throw if version tag does not match npm tag (version tag = "latest", npm tag = "nightly")', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + version: '1.0.0' } ); - it( 'should throw if version tag does not match npm tag (version tag = "alpha", npm tag = "staging")', () => { - stubs.fs.readJson.withArgs( 'ckeditor5-foo/package.json' ).resolves( { - name: 'ckeditor5-foo', - version: '1.0.0-alpha.0' - } ); + await expect( assertNpmTag( [ 'ckeditor5-foo' ], 'nightly' ) ) + .rejects.toThrow( 'The version tag "latest" from "ckeditor5-foo" package does not match the npm tag "nightly".' ); + } ); - return assertNpmTag( [ 'ckeditor5-foo' ], 'staging' ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'The version tag "alpha" from "ckeditor5-foo" package does not match the npm tag "staging".' - ); - } ); + it( 'should throw if version tag does not match npm tag (version tag = "alpha", npm tag = "staging")', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + version: '1.0.0-alpha.0' } ); - it( 'should throw if version tag does not match npm tag (version tag = "nightly", npm tag = "staging")', () => { - stubs.fs.readJson.withArgs( 'ckeditor5-foo/package.json' ).resolves( { - name: 'ckeditor5-foo', - version: '0.0.0-nightly-20230517.0' - } ); + await expect( assertNpmTag( [ 'ckeditor5-foo' ], 'staging' ) ) + .rejects.toThrow( 'The version tag "alpha" from "ckeditor5-foo" package does not match the npm tag "staging".' ); + } ); - return assertNpmTag( [ 'ckeditor5-foo' ], 'staging' ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'The version tag "nightly" from "ckeditor5-foo" package does not match the npm tag "staging".' - ); - } ); + it( 'should throw if version tag does not match npm tag (version tag = "nightly", npm tag = "staging")', async () => { + vi.mocked( fs ).readJson.mockResolvedValue( { + name: 'ckeditor5-foo', + version: '0.0.0-nightly-20230517.0' } ); - it( 'should throw one error for all packages with incorrect tags', () => { - stubs.fs.readJson - .withArgs( 'ckeditor5-foo/package.json' ).resolves( { + await expect( assertNpmTag( [ 'ckeditor5-foo' ], 'staging' ) ) + .rejects.toThrow( 'The version tag "nightly" from "ckeditor5-foo" package does not match the npm tag "staging".' ); + } ); + + it( 'should throw one error for all packages with incorrect tags', async () => { + vi.mocked( fs ).readJson.mockImplementation( input => { + if ( input === 'ckeditor5-foo/package.json' ) { + return Promise.resolve( { name: 'ckeditor5-foo', version: '1.0.0-alpha' - } ) - .withArgs( 'ckeditor5-bar/package.json' ).resolves( { + } ); + } + + if ( input === 'ckeditor5-bar/package.json' ) { + return Promise.resolve( { name: 'ckeditor5-bar', version: '0.0.0-nightly-20230517.0' - } ) - .withArgs( 'ckeditor5-baz/package.json' ).resolves( { - name: 'ckeditor5-baz', - version: '0.0.1-rc.5' } ); + } - return assertNpmTag( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ], 'latest' ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'The version tag "alpha" from "ckeditor5-foo" package does not match the npm tag "latest".\n' + - 'The version tag "nightly" from "ckeditor5-bar" package does not match the npm tag "latest".\n' + - 'The version tag "rc" from "ckeditor5-baz" package does not match the npm tag "latest".' - ); - } ); + return Promise.resolve( { + name: 'ckeditor5-baz', + version: '0.0.1-rc.5' + } ); } ); + + const errorMessage = 'The version tag "alpha" from "ckeditor5-foo" package does not match the npm tag "latest".\n' + + 'The version tag "nightly" from "ckeditor5-bar" package does not match the npm tag "latest".\n' + + 'The version tag "rc" from "ckeditor5-baz" package does not match the npm tag "latest".'; + + await expect( assertNpmTag( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ], 'latest' ) ) + .rejects.toThrow( errorMessage ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/assertpackages.js b/packages/ckeditor5-dev-release-tools/tests/utils/assertpackages.js index f23cebbb0..7d80a4d37 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/assertpackages.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/assertpackages.js @@ -3,126 +3,92 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi } from 'vitest'; +import fs from 'fs-extra'; +import assertPackages from '../../lib/utils/assertpackages.js'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +vi.mock( 'fs-extra' ); -describe( 'dev-release-tools/utils', () => { - describe( 'assertPackages()', () => { - let assertPackages, sandbox, stubs; +describe( 'assertPackages()', () => { + const disableMainValidatorOptions = { + requireEntryPoint: false, + optionalEntryPointPackages: [] + }; - const disableMainValidatorOptions = { - requireEntryPoint: false, - optionalEntryPointPackages: [] - }; + it( 'should resolve the promise if list of packages is empty', async () => { + await assertPackages( [], { ...disableMainValidatorOptions } ); + } ); - beforeEach( () => { - sandbox = sinon.createSandbox(); + it( 'should check if `package.json` exists in each package', async () => { + vi.mocked( fs ).pathExists.mockResolvedValue( true ); - stubs = { - fs: { - pathExists: sandbox.stub(), - readJson: sandbox.stub() - } - }; + await assertPackages( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ], { ...disableMainValidatorOptions } ); - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); + expect( vi.mocked( fs ).pathExists ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs ).pathExists ).toHaveBeenCalledWith( 'ckeditor5-foo/package.json' ); + expect( vi.mocked( fs ).pathExists ).toHaveBeenCalledWith( 'ckeditor5-bar/package.json' ); + expect( vi.mocked( fs ).pathExists ).toHaveBeenCalledWith( 'ckeditor5-baz/package.json' ); + } ); - mockery.registerMock( 'fs-extra', stubs.fs ); + it( 'should throw one error for all packages with missing `package.json` file', async () => { + vi.mocked( fs ).pathExists.mockImplementation( input => { + if ( input === 'ckeditor5-bar/package.json' ) { + return Promise.resolve( true ); + } - assertPackages = require( '../../lib/utils/assertpackages' ); + return Promise.resolve( false ); } ); - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); - } ); - - it( 'should resolve the promise if list of packages is empty', () => { - return assertPackages( [], { ...disableMainValidatorOptions } ); - } ); + await expect( assertPackages( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ], { ...disableMainValidatorOptions } ) ) + .rejects.toThrow( + 'The "package.json" file is missing in the "ckeditor5-foo" package.\n' + + 'The "package.json" file is missing in the "ckeditor5-baz" package.' + ); + } ); - it( 'should check if `package.json` exists in each package', () => { - stubs.fs.pathExists.resolves( true ); + // See: https://github.com/ckeditor/ckeditor5/issues/15127. + describe( 'the entry package point validator', () => { + const enableMainValidatorOptions = { + requireEntryPoint: true, + optionalEntryPointPackages: [ + '@ckeditor/ckeditor5-bar' + ] + }; - return assertPackages( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ], { ...disableMainValidatorOptions } ) - .then( () => { - expect( stubs.fs.pathExists.callCount ).to.equal( 3 ); - expect( stubs.fs.pathExists.firstCall.args[ 0 ] ).to.equal( 'ckeditor5-foo/package.json' ); - expect( stubs.fs.pathExists.secondCall.args[ 0 ] ).to.equal( 'ckeditor5-bar/package.json' ); - expect( stubs.fs.pathExists.thirdCall.args[ 0 ] ).to.equal( 'ckeditor5-baz/package.json' ); - } ); - } ); + it( 'should throw if a package misses its entry point', async () => { + vi.mocked( fs ).pathExists.mockResolvedValue( true ); + vi.mocked( fs ).readJson.mockImplementation( input => { + if ( input === 'ckeditor5-foo/package.json' ) { + return Promise.resolve( { + name: '@ckeditor/ckeditor5-foo', + main: 'src/index.ts' + } ); + } - it( 'should throw one error for all packages with missing `package.json` file', () => { - stubs.fs.pathExists - .resolves( false ) - .withArgs( 'ckeditor5-bar/package.json' ).resolves( true ); - - return assertPackages( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ], { ...disableMainValidatorOptions } ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'The "package.json" file is missing in the "ckeditor5-foo" package.\n' + - 'The "package.json" file is missing in the "ckeditor5-baz" package.' - ); + if ( input === 'ckeditor5-bar/package.json' ) { + return Promise.resolve( { + name: '@ckeditor/ckeditor5-bar' } ); - } ); + } - // See: https://github.com/ckeditor/ckeditor5/issues/15127. - describe( 'the entry package point validator', () => { - const enableMainValidatorOptions = { - requireEntryPoint: true, - optionalEntryPointPackages: [ - '@ckeditor/ckeditor5-bar' - ] - }; - - it( 'should throw if a package misses its entry point', () => { - stubs.fs.pathExists.resolves( true ); - stubs.fs.readJson.withArgs( 'ckeditor5-foo/package.json' ).resolves( { - name: '@ckeditor/ckeditor5-foo', - main: 'src/index.ts' - } ); - stubs.fs.readJson.withArgs( 'ckeditor5-bar/package.json' ).resolves( { - name: '@ckeditor/ckeditor5-bar' - } ); - stubs.fs.readJson.withArgs( 'ckeditor5-baz/package.json' ).resolves( { + return Promise.resolve( { name: '@ckeditor/ckeditor5-baz' } ); - - return assertPackages( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ], { ...enableMainValidatorOptions } ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( - 'The "@ckeditor/ckeditor5-baz" package misses the entry point ("main") definition in its "package.json".' - ); - } ); } ); - it( 'should pass the validator if specified package does not have to define the entry point', () => { - stubs.fs.pathExists.resolves( true ); - stubs.fs.readJson.withArgs( 'ckeditor5-bar/package.json' ).resolves( { - name: '@ckeditor/ckeditor5-bar' - } ); + await expect( assertPackages( [ 'ckeditor5-foo', 'ckeditor5-bar', 'ckeditor5-baz' ], { ...enableMainValidatorOptions } ) ) + .rejects.toThrow( + 'The "@ckeditor/ckeditor5-baz" package misses the entry point ("main") definition in its "package.json".' + ); + } ); - return assertPackages( [ 'ckeditor5-bar' ], { ...enableMainValidatorOptions } ); + it( 'should pass the validator if specified package does not have to define the entry point', async () => { + vi.mocked( fs ).pathExists.mockResolvedValue( true ); + vi.mocked( fs ).readJson.mockResolvedValue( { + name: '@ckeditor/ckeditor5-bar' } ); + + await assertPackages( [ 'ckeditor5-bar' ], { ...enableMainValidatorOptions } ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/changelog.js b/packages/ckeditor5-dev-release-tools/tests/utils/changelog.js deleted file mode 100644 index 2a5423207..000000000 --- a/packages/ckeditor5-dev-release-tools/tests/utils/changelog.js +++ /dev/null @@ -1,437 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); - -describe( 'dev-release-tools/utils', () => { - let utils, sandbox; - - describe( 'changelog', () => { - beforeEach( () => { - sandbox = sinon.createSandbox(); - - sandbox.stub( - require( '../../lib/utils/transformcommitutils' ), - 'getRepositoryUrl' - ).returns( 'https://github.com/ckeditor/ckeditor5-dev' ); - - utils = require( '../../lib/utils/changelog' ); - } ); - - afterEach( () => { - sandbox.restore(); - } ); - - it( 'should define constants', () => { - expect( utils.changelogFile ).to.be.a( 'string' ); - expect( utils.changelogHeader ).to.be.a( 'string' ); - } ); - - describe( 'getChangesForVersion()', () => { - it( 'returns changes for the first tag which is a link to the release', () => { - const expectedChangelog = [ - '### Features', - '', - '* Cloned the main module. ([abcd123](https://github.com))' - ].join( '\n' ); - - const changelog = [ - '## [0.1.0](https://github.com) (2017-01-13)', - '', - expectedChangelog - ].join( '\n' ); - - const currentChangelogStub = sandbox.stub( utils, 'getChangelog' ) - .returns( utils.changelogHeader + changelog ); - - const parsedChangelog = utils.getChangesForVersion( 'v0.1.0' ); - - expect( currentChangelogStub.calledOnce ).to.equal( true ); - expect( parsedChangelog ).to.equal( expectedChangelog ); - } ); - - it( 'returns changes for the first tag which is not a link', () => { - const expectedChangelog = [ - '### Features', - '', - '* Cloned the main module. ([abcd123](https://github.com))' - ].join( '\n' ); - - const changelog = [ - '## 0.1.0 (2017-01-13)', - '', - expectedChangelog - ].join( '\n' ); - - const currentChangelogStub = sandbox.stub( utils, 'getChangelog' ) - .returns( utils.changelogHeader + changelog ); - - const parsedChangelog = utils.getChangesForVersion( 'v0.1.0' ); - - expect( currentChangelogStub.calledOnce ).to.equal( true ); - expect( parsedChangelog ).to.equal( expectedChangelog ); - } ); - - it( 'returns changes between tags', () => { - const expectedChangelog = [ - '### Features', - '', - '* Cloned the main module. ([abcd123](https://github.com))', - '', - '### BREAKING CHANGE', - '', - '* Bump the major!' - ].join( '\n' ); - - const changelog = [ - '## [1.0.0](https://github.com/) (2017-01-13)', - '', - expectedChangelog, - '', - '## [0.1.0](https://github.com) (2017-01-13)', - '', - '### Features', - '', - '* Cloned the main module. ([abcd123](https://github.com))' - ].join( '\n' ); - - const currentChangelogStub = sandbox.stub( utils, 'getChangelog' ) - .returns( utils.changelogHeader + changelog ); - - const parsedChangelog = utils.getChangesForVersion( 'v1.0.0' ); - - expect( currentChangelogStub.calledOnce ).to.equal( true ); - expect( parsedChangelog ).to.equal( expectedChangelog ); - } ); - - it( 'returns null if cannot find changes for the specified version', () => { - const changelog = [ - '## [0.1.0](https://github.com) (2017-01-13)', - '', - '### Features', - '', - '* Cloned the main module. ([abcd123](https://github.com))' - ].join( '\n' ); - - sandbox.stub( utils, 'getChangelog' ) - .returns( utils.changelogHeader + changelog ); - - expect( utils.getChangesForVersion( 'v1.0.0' ) ).to.equal( null ); - } ); - - it( 'works when date is not specified', () => { - const changelog = [ - '## 0.3.0', - '', - 'Foo' - ].join( '\n' ); - - sandbox.stub( utils, 'getChangelog' ) - .returns( utils.changelogHeader + changelog ); - - expect( utils.getChangesForVersion( 'v0.3.0' ) ) - .to.equal( 'Foo' ); - } ); - - it( 'captures correct range of changes (headers are URLs)', () => { - const changelog = [ - '## [0.3.0](https://github.com) (2017-01-13)', - '', - '3', - '', - 'Some text ## [like a release header]', - '', - '## [0.2.0](https://github.com) (2017-01-13)', - '', - '2', - '', - '## [0.1.0](https://github.com) (2017-01-13)', - '', - '1' - ].join( '\n' ); - - sandbox.stub( utils, 'getChangelog' ) - .returns( utils.changelogHeader + changelog ); - - expect( utils.getChangesForVersion( 'v0.3.0' ) ) - .to.equal( '3\n\nSome text ## [like a release header]' ); - - expect( utils.getChangesForVersion( 'v0.2.0' ) ) - .to.equal( '2' ); - } ); - - it( 'captures correct range of changes (headers are plain text, "the initial" release check)', () => { - const changelog = [ - 'Changelog', - '=========', - '', - '## 1.0.2 (2022-02-22)', - '', - '### Other changes', - '', - '* Other change for `1.0.2`.', - '', - '', - '## 1.0.1 (2022-02-22)', - '', - '### Other changes', - '', - '* Other change for `1.0.1`.', - '', - '', - '## 1.0.0 (2022-02-22)', - '', - 'This is the initial release.' - ].join( '\n' ); - - sandbox.stub( utils, 'getChangelog' ).returns( utils.changelogHeader + changelog ); - - expect( utils.getChangesForVersion( '1.0.0' ) ).to.equal( 'This is the initial release.' ); - } ); - - it( 'captures correct range of changes (headers are plain text, "middle" version check)', () => { - const changelog = [ - 'Changelog', - '=========', - '', - '## 1.0.2 (2022-02-22)', - '', - '### Other changes', - '', - '* Other change for `1.0.2`.', - '', - '', - '## 1.0.1 (2022-02-22)', - '', - '### Other changes', - '', - '* Other change for `1.0.1`.', - '', - '', - '## 1.0.0 (2022-02-22)', - '', - 'This is the initial release.' - ].join( '\n' ); - - sandbox.stub( utils, 'getChangelog' ).returns( utils.changelogHeader + changelog ); - - expect( utils.getChangesForVersion( '1.0.1' ) ).to.equal( [ - '### Other changes', - '', - '* Other change for `1.0.1`.' - ].join( '\n' ) ); - } ); - - it( 'captures correct range of changes (headers are plain text, "the latest" check)', () => { - const changelog = [ - 'Changelog', - '=========', - '', - '## 1.0.2 (2022-02-22)', - '', - '### Other changes', - '', - '* Other change for `1.0.2`.', - '', - '', - '## 1.0.1 (2022-02-22)', - '', - '### Other changes', - '', - '* Other change for `1.0.1`.', - '', - '', - '## 1.0.0 (2022-02-22)', - '', - 'This is the initial release.' - ].join( '\n' ); - - sandbox.stub( utils, 'getChangelog' ).returns( utils.changelogHeader + changelog ); - - expect( utils.getChangesForVersion( '1.0.2' ) ).to.equal( [ - '### Other changes', - '', - '* Other change for `1.0.2`.' - ].join( '\n' ) ); - } ); - } ); - - describe( 'getChangelog()', () => { - it( 'resolves the changelog', () => { - const joinStub = sandbox.stub( path, 'join' ).returns( 'path-to-changelog' ); - const existsSyncStub = sandbox.stub( fs, 'existsSync' ).returns( true ); - const readFileStub = sandbox.stub( fs, 'readFileSync' ).returns( 'Content.' ); - const changelog = utils.getChangelog(); - - expect( joinStub.calledOnce ).to.equal( true ); - expect( existsSyncStub.calledOnce ).to.equal( true ); - expect( readFileStub.calledOnce ).to.equal( true ); - expect( readFileStub.firstCall.args[ 0 ] ).to.equal( 'path-to-changelog' ); - expect( readFileStub.firstCall.args[ 1 ] ).to.equal( 'utf-8' ); - expect( changelog ).to.equal( 'Content.' ); - } ); - - it( 'returns null if the changelog does not exist', () => { - const joinStub = sandbox.stub( path, 'join' ).returns( 'path-to-changelog' ); - const existsSyncStub = sandbox.stub( fs, 'existsSync' ).returns( false ); - const readFileStub = sandbox.stub( fs, 'readFileSync' ); - const changelog = utils.getChangelog(); - - expect( joinStub.calledOnce ).to.equal( true ); - expect( existsSyncStub.calledOnce ).to.equal( true ); - expect( readFileStub.called ).to.equal( false ); - expect( changelog ).to.equal( null ); - } ); - } ); - - describe( 'saveChangelog()', () => { - it( 'saves the changelog', () => { - const processCwdStub = sandbox.stub( process, 'cwd' ).returns( '/tmp' ); - const joinStub = sandbox.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ); - const writeFileStub = sandbox.stub( fs, 'writeFileSync' ); - - utils.saveChangelog( 'New content.' ); - - expect( joinStub.calledOnce ).to.equal( true ); - expect( processCwdStub.calledOnce ).to.equal( true ); - expect( writeFileStub.calledOnce ).to.equal( true ); - expect( writeFileStub.firstCall.args[ 0 ] ).to.equal( '/tmp/CHANGELOG.md' ); - expect( writeFileStub.firstCall.args[ 1 ] ).to.equal( 'New content.' ); - } ); - - it( 'allows changing cwd', () => { - const joinStub = sandbox.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ); - const writeFileStub = sandbox.stub( fs, 'writeFileSync' ); - - utils.saveChangelog( 'New content.', '/new-cwd' ); - - expect( joinStub.calledOnce ).to.equal( true ); - expect( writeFileStub.calledOnce ).to.equal( true ); - expect( writeFileStub.firstCall.args[ 0 ] ).to.equal( '/new-cwd/CHANGELOG.md' ); - expect( writeFileStub.firstCall.args[ 1 ] ).to.equal( 'New content.' ); - } ); - } ); - - describe( 'truncateChangelog()', () => { - it( 'does nothing if there is no changelog', () => { - const saveChangelogStub = sandbox.stub( utils, 'saveChangelog' ); - - sandbox.stub( utils, 'getChangelog' ).returns( null ); - - utils.truncateChangelog( 5 ); - - expect( saveChangelogStub.called ).to.equal( false ); - } ); - - it( 'does nothing if changelog does not contain entries', () => { - const saveChangelogStub = sandbox.stub( utils, 'saveChangelog' ); - - sandbox.stub( utils, 'getChangelog' ).returns( utils.changelogHeader + '\n\n' ); - - utils.truncateChangelog( 5 ); - - expect( saveChangelogStub.called ).to.equal( false ); - } ); - - it( 'truncates the changelog and adds the link to the release page', () => { - const expectedChangelogEntries = [ - '## [0.3.0](https://github.com) (2017-01-13)', - '', - '3', - '', - 'Some text ## [like a release header]', - '', - '## [0.2.0](https://github.com) (2017-01-13)', - '', - '2' - ].join( '\n' ); - - const expectedChangelogFooter = [ - '', - '', - '---', - '', - 'To see all releases, visit the [release page](https://github.com/ckeditor/ckeditor5-dev/releases).', - '' - ].join( '\n' ); - - const changelogEntries = [ - expectedChangelogEntries, - '', - '## [0.1.0](https://github.com) (2017-01-13)', - '', - '1' - ].join( '\n' ); - - const saveChangelogStub = sandbox.stub( utils, 'saveChangelog' ); - - sandbox.stub( utils, 'getChangelog' ).returns( utils.changelogHeader + changelogEntries ); - - utils.truncateChangelog( 2 ); - - expect( saveChangelogStub.calledOnce ).to.equal( true ); - expect( saveChangelogStub.firstCall.args[ 0 ] ).to.equal( - utils.changelogHeader + - expectedChangelogEntries + - expectedChangelogFooter - ); - } ); - - it( 'does not add the link to the release page if changelog is not truncated', () => { - const expectedChangelogEntries = [ - '## [0.3.0](https://github.com) (2017-01-13)', - '', - '3', - '', - 'Some text ## [like a release header]', - '', - '## [0.2.0](https://github.com) (2017-01-13)', - '', - '2' - ].join( '\n' ); - - const expectedChangelogFooter = '\n'; - - const changelogEntries = expectedChangelogEntries; - - const saveChangelogStub = sandbox.stub( utils, 'saveChangelog' ); - - sandbox.stub( utils, 'getChangelog' ).returns( utils.changelogHeader + changelogEntries ); - - utils.truncateChangelog( 2 ); - - expect( saveChangelogStub.calledOnce ).to.equal( true ); - expect( saveChangelogStub.firstCall.args[ 0 ] ).to.equal( - utils.changelogHeader + - expectedChangelogEntries + - expectedChangelogFooter - ); - } ); - } ); - - describe( 'getFormattedDate()', () => { - let clock; - - beforeEach( () => { - clock = sinon.useFakeTimers( { - now: new Date( '2023-06-15 12:00:00' ) - } ); - } ); - - afterEach( () => { - clock.restore(); - } ); - - it( 'returns a date following the format "year-month-day" with the leading zeros', () => { - expect( utils.getFormattedDate() ).to.equal( '2023-06-15' ); - } ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/checkversionavailability.js b/packages/ckeditor5-dev-release-tools/tests/utils/checkversionavailability.js index c0ea6d222..d0fa6c092 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/checkversionavailability.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/checkversionavailability.js @@ -3,99 +3,61 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); - -describe( 'dev-release-tools/utils', () => { - let checkVersionAvailability, sandbox, stubs; - - describe( 'checkVersionAvailability()', () => { - beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - shExec: sandbox.stub(), - shellEscape: sinon.stub().callsFake( v => v[ 0 ] ) - }; - - checkVersionAvailability = proxyquire( '../../lib/utils/checkversionavailability.js', { - '@ckeditor/ckeditor5-dev-utils': { - tools: { - shExec: stubs.shExec - } - }, - 'shell-escape': stubs.shellEscape - } ); - } ); - - afterEach( () => { - sandbox.restore(); - } ); - - it( 'should resolve to true if version does not exist (npm >= 8.13.0 && npm < 10.0.0)', () => { - stubs.shExec.rejects( new Error( 'npm ERR! code E404' ) ); - - return checkVersionAvailability( '1.0.1', 'stub-package' ) - .then( result => { - expect( stubs.shExec.callCount ).to.equal( 1 ); - expect( stubs.shExec.firstCall.args[ 0 ] ).to.equal( 'npm show stub-package@1.0.1 version' ); - expect( result ).to.be.true; - } ); - } ); - - it( 'should resolve to true if version does not exist (npm >= 10.0.0)', () => { - stubs.shExec.rejects( new Error( 'npm error code E404' ) ); - - return checkVersionAvailability( '1.0.1', 'stub-package' ) - .then( result => { - expect( stubs.shExec.callCount ).to.equal( 1 ); - expect( stubs.shExec.firstCall.args[ 0 ] ).to.equal( 'npm show stub-package@1.0.1 version' ); - expect( result ).to.be.true; - } ); - } ); - - it( 'should resolve to true if version does not exist (npm < 8.13.0)', () => { - stubs.shExec.resolves(); - - return checkVersionAvailability( '1.0.1', 'stub-package' ) - .then( result => { - expect( result ).to.be.true; - } ); - } ); - - it( 'should resolve to false if version exists', () => { - stubs.shExec.resolves( '1.0.1' ); - - return checkVersionAvailability( '1.0.1', 'stub-package' ) - .then( result => { - expect( result ).to.be.false; - } ); - } ); - - it( 'should re-throw an error if unknown error occured', () => { - stubs.shExec.rejects( new Error( 'Unknown error.' ) ); - - return checkVersionAvailability( '1.0.1', 'stub-package' ) - .then( () => { - throw new Error( 'Expected to be rejected.' ); - } ) - .catch( error => { - expect( error.message ).to.equal( 'Unknown error.' ); - } ); - } ); - - it( 'should escape arguments passed to a shell command', async () => { - stubs.shExec.rejects( new Error( 'npm ERR! code E404' ) ); - - return checkVersionAvailability( '1.0.1', 'stub-package' ) - .then( () => { - expect( stubs.shellEscape.callCount ).to.equal( 2 ); - expect( stubs.shellEscape.firstCall.firstArg ).to.deep.equal( [ 'stub-package' ] ); - expect( stubs.shellEscape.secondCall.firstArg ).to.deep.equal( [ '1.0.1' ] ); - } ); - } ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import shellEscape from 'shell-escape'; +import checkVersionAvailability from '../../lib/utils/checkversionavailability.js'; + +vi.mock( 'shell-escape' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); + +describe( 'checkVersionAvailability()', () => { + beforeEach( () => { + vi.mocked( shellEscape ).mockImplementation( v => v[ 0 ] ); + } ); + + it( 'should resolve to true if version does not exist (npm >= 8.13.0 && npm < 10.0.0)', async () => { + vi.mocked( tools ).shExec.mockRejectedValue( new Error( 'npm ERR! code E404' ) ); + + const result = await checkVersionAvailability( '1.0.1', 'stub-package' ); + + expect( vi.mocked( tools ).shExec ).toHaveBeenCalledExactlyOnceWith( 'npm show stub-package@1.0.1 version', expect.any( Object ) ); + expect( result ).toBe( true ); + } ); + + it( 'should resolve to true if version does not exist (npm >= 10.0.0)', async () => { + vi.mocked( tools ).shExec.mockRejectedValue( new Error( 'npm error code E404' ) ); + + const result = await checkVersionAvailability( '1.0.1', 'stub-package' ); + expect( vi.mocked( tools ).shExec ).toHaveBeenCalledExactlyOnceWith( 'npm show stub-package@1.0.1 version', expect.any( Object ) ); + expect( result ).toBe( true ); + } ); + + it( 'should resolve to true if version does not exist (npm < 8.13.0)', async () => { + vi.mocked( tools ).shExec.mockResolvedValue( '' ); + + await expect( checkVersionAvailability( '1.0.1', 'stub-package' ) ).resolves.toBe( true ); + } ); + + it( 'should resolve to false if version exists', async () => { + vi.mocked( tools ).shExec.mockResolvedValue( '1.0.1' ); + + await expect( checkVersionAvailability( '1.0.1', 'stub-package' ) ).resolves.toBe( false ); + } ); + + it( 'should re-throw an error if unknown error occurred', async () => { + vi.mocked( tools ).shExec.mockRejectedValue( new Error( 'Unknown error.' ) ); + + await expect( checkVersionAvailability( '1.0.1', 'stub-package' ) ) + .rejects.toThrow( 'Unknown error.' ); + } ); + + it( 'should escape arguments passed to a shell command', async () => { + vi.mocked( tools ).shExec.mockRejectedValue( new Error( 'npm ERR! code E404' ) ); + + await checkVersionAvailability( '1.0.1', 'stub-package' ); + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledWith( [ 'stub-package' ] ); + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledWith( [ '1.0.1' ] ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/cli.js b/packages/ckeditor5-dev-release-tools/tests/utils/cli.js deleted file mode 100644 index 9641fba91..000000000 --- a/packages/ckeditor5-dev-release-tools/tests/utils/cli.js +++ /dev/null @@ -1,476 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); - -describe( 'dev-release-tools/utils', () => { - let cli, sandbox, questionItems, userAnswer, stub; - - describe( 'cli', () => { - beforeEach( () => { - userAnswer = undefined; - sandbox = sinon.createSandbox(); - questionItems = []; - - stub = { - chalk: { - red: sandbox.stub().callsFake( str => str ), - magenta: sandbox.stub().callsFake( str => str ), - cyan: sandbox.stub().callsFake( str => str ), - underline: sandbox.stub().callsFake( str => str ), - gray: sandbox.stub().callsFake( str => str ) - } - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'inquirer', { - prompt( questions ) { - questionItems.push( ...questions ); - const questionItem = questions[ 0 ]; - - // If `userAnswer` is undefined, return a suggested value as a user input. - return Promise.resolve( { - [ questionItem.name ]: typeof userAnswer != 'undefined' ? userAnswer : questionItem.default - } ); - } - } ); - - mockery.registerMock( 'chalk', stub.chalk ); - - cli = require( '../../lib/utils/cli' ); - } ); - - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); - } ); - - describe( 'INDENT_SIZE', () => { - it( 'is defined', () => { - expect( cli.INDENT_SIZE ).to.be.a( 'Number' ); - } ); - } ); - - describe( 'COMMIT_INDENT_SIZE', () => { - it( 'is defined', () => { - expect( cli.COMMIT_INDENT_SIZE ).to.be.a( 'Number' ); - } ); - } ); - - describe( 'confirmUpdatingVersions()', () => { - it( 'displays packages and their versions (current and proposed) to release', () => { - const packagesMap = new Map(); - - packagesMap.set( '@ckeditor/ckeditor5-engine', { - previousVersion: '1.0.0', - version: '1.1.0' - } ); - packagesMap.set( '@ckeditor/ckeditor5-core', { - previousVersion: '0.7.0', - version: '0.7.1' - } ); - - return cli.confirmUpdatingVersions( packagesMap ) - .then( () => { - const question = questionItems[ 0 ]; - - expect( question.message ).to.match( /^Packages and their old and new versions:/ ); - expect( question.message ).to.match( /"@ckeditor\/ckeditor5-engine": v1\.0\.0 => v1\.1\.0/ ); - expect( question.message ).to.match( /"@ckeditor\/ckeditor5-core": v0\.7\.0 => v0\.7\.1/ ); - expect( question.message ).to.match( /Continue\?$/ ); - } ); - } ); - - it( 'sorts the packages alphabetically', () => { - const packagesMap = new Map(); - - packagesMap.set( '@ckeditor/ckeditor5-list', {} ); - packagesMap.set( '@ckeditor/ckeditor5-autoformat', {} ); - packagesMap.set( '@ckeditor/ckeditor5-basic-styles', {} ); - packagesMap.set( '@ckeditor/ckeditor5-core', {} ); - packagesMap.set( '@ckeditor/ckeditor5-link', {} ); - packagesMap.set( '@ckeditor/ckeditor5-build-classic', {} ); - - return cli.confirmUpdatingVersions( packagesMap ) - .then( () => { - const packagesAsArray = questionItems[ 0 ].message - .split( '\n' ) - // Remove header and footer from the message. - .slice( 1, -1 ) - // Extract package name from the whole line. - .map( line => line.replace( /.*"([^"]+)".*/, '$1' ) ); - - expect( packagesAsArray.length ).to.equal( 6 ); - expect( packagesAsArray[ 0 ] ).to.equal( '@ckeditor/ckeditor5-autoformat' ); - expect( packagesAsArray[ 1 ] ).to.equal( '@ckeditor/ckeditor5-basic-styles' ); - expect( packagesAsArray[ 2 ] ).to.equal( '@ckeditor/ckeditor5-build-classic' ); - expect( packagesAsArray[ 3 ] ).to.equal( '@ckeditor/ckeditor5-core' ); - expect( packagesAsArray[ 4 ] ).to.equal( '@ckeditor/ckeditor5-link' ); - expect( packagesAsArray[ 5 ] ).to.equal( '@ckeditor/ckeditor5-list' ); - } ); - } ); - } ); - - describe( 'confirmPublishing()', () => { - it( 'displays packages and services where they should be released', () => { - const packagesMap = new Map(); - - packagesMap.set( '@ckeditor/ckeditor5-engine', { - version: '1.1.0', - shouldReleaseOnNpm: true, - shouldReleaseOnGithub: true - } ); - packagesMap.set( '@ckeditor/ckeditor5-core', { - version: '0.7.0', - shouldReleaseOnNpm: false, - shouldReleaseOnGithub: true - } ); - packagesMap.set( '@ckeditor/ckeditor5-utils', { - version: '1.7.0', - shouldReleaseOnNpm: true, - shouldReleaseOnGithub: false - } ); - packagesMap.set( '@ckeditor/ckeditor5-widget', { - version: '2.0.0', - shouldReleaseOnNpm: false, - shouldReleaseOnGithub: false - } ); - - return cli.confirmPublishing( packagesMap ) - .then( () => { - const question = questionItems[ 0 ]; - - expect( question.message ).to.match( /^Services where the release will be created:/ ); - expect( question.message ).to.match( /"@ckeditor\/ckeditor5-core" - version: 0\.7\.0 - services: GitHub/ ); - expect( question.message ).to.match( /"@ckeditor\/ckeditor5-engine" - version: 1\.1\.0 - services: NPM, GitHub/ ); - expect( question.message ).to.match( /"@ckeditor\/ckeditor5-utils" - version: 1\.7\.0 - services: NPM/ ); - expect( question.message ).to.match( /"@ckeditor\/ckeditor5-widget" - version: 2\.0\.0 - nothing to release/ ); - expect( question.message ).to.match( /Continue\?$/ ); - } ); - } ); - } ); - - describe( 'confirmRemovingFiles()', () => { - it( 'user can disagree with the proposed value', () => { - return cli.confirmRemovingFiles() - .then( () => { - const question = questionItems[ 0 ]; - - expect( question.message ).to.match( /^Remove created archives\?/ ); - expect( question.type ).to.equal( 'confirm' ); - } ); - } ); - } ); - - describe( 'confirmIncludingPackage()', () => { - it( 'user can disagree with the proposed value', () => { - return cli.confirmIncludingPackage() - .then( () => { - const question = questionItems[ 0 ]; - - expect( question.message ).to.match( - /^Package does not contain all required files to publish. Include this package in the release and continue\?/ - ); - expect( question.type ).to.equal( 'confirm' ); - } ); - } ); - } ); - - describe( 'provideVersion()', () => { - it( 'suggests specified version', () => { - return cli.provideVersion( '1.0.0', '1.1.0' ) - .then( newVersion => { - expect( newVersion ).to.equal( '1.1.0' ); - } ); - } ); - - it( 'should suggest proper "major" version for public package', () => { - return cli.provideVersion( '1.0.0', 'major' ) - .then( newVersion => { - expect( newVersion ).to.equal( '2.0.0' ); - } ); - } ); - - it( 'should suggest proper "minor" version for public package', () => { - return cli.provideVersion( '1.0.0', 'minor' ) - .then( newVersion => { - expect( questionItems[ 0 ].message ).to.equal( - 'Type the new version, "skip" or "internal" (suggested: "1.1.0", current: "1.0.0"):' - ); - - expect( newVersion ).to.equal( '1.1.0' ); - } ); - } ); - - it( 'should suggest proper "patch" version for public package', () => { - return cli.provideVersion( '1.0.0', 'patch' ) - .then( newVersion => { - expect( newVersion ).to.equal( '1.0.1' ); - } ); - } ); - - it( 'should suggest "skip" version for package which does not contain changes (proposed null)', () => { - return cli.provideVersion( '1.0.0', null ) - .then( newVersion => { - expect( newVersion ).to.equal( 'skip' ); - } ); - } ); - - it( 'should suggest "skip" version for package which does not contain changes (proposed "skip")', () => { - return cli.provideVersion( '1.0.0', 'skip' ) - .then( newVersion => { - expect( newVersion ).to.equal( 'skip' ); - } ); - } ); - - it( 'should suggest "minor" instead of "major" version for non-public package', () => { - return cli.provideVersion( '0.7.0', 'major' ) - .then( newVersion => { - expect( newVersion ).to.equal( '0.8.0' ); - } ); - } ); - - it( 'should suggest proper "patch" version for non-public package', () => { - return cli.provideVersion( '0.7.0', 'patch' ) - .then( newVersion => { - expect( newVersion ).to.equal( '0.7.1' ); - } ); - } ); - - it( 'returns "internal" if suggested version was "internal"', () => { - return cli.provideVersion( '0.1.0', 'internal' ) - .then( newVersion => { - expect( newVersion ).to.equal( 'internal' ); - } ); - } ); - - it( 'allows disabling "internal" version', () => { - return cli.provideVersion( '0.1.0', 'major', { disableInternalVersion: true } ) - .then( () => { - expect( questionItems[ 0 ].message ).to.equal( - 'Type the new version or "skip" (suggested: "0.2.0", current: "0.1.0"):' - ); - } ); - } ); - - it( 'returns "skip" if suggested version was "internal" but it is disabled', () => { - return cli.provideVersion( '0.1.0', 'internal', { disableInternalVersion: true } ) - .then( newVersion => { - expect( newVersion ).to.equal( 'skip' ); - } ); - } ); - - it( 'should suggest proper pre-release version for pre-release package (major bump)', () => { - return cli.provideVersion( '1.0.0-alpha.1', 'major' ) - .then( newVersion => { - expect( newVersion ).to.equal( '1.0.0-alpha.2' ); - } ); - } ); - - it( 'should suggest proper pre-release version for pre-release package (minor bump)', () => { - return cli.provideVersion( '1.0.0-alpha.1', 'minor' ) - .then( newVersion => { - expect( newVersion ).to.equal( '1.0.0-alpha.2' ); - } ); - } ); - - it( 'should suggest proper pre-release version for pre-release package (patch bump)', () => { - return cli.provideVersion( '1.0.0-alpha.1', 'patch' ) - .then( newVersion => { - expect( newVersion ).to.equal( '1.0.0-alpha.2' ); - } ); - } ); - - it( 'removes spaces from provided version', () => { - return cli.provideVersion( '1.0.0', 'major' ) - .then( () => { - const { filter } = questionItems[ 0 ]; - - expect( filter( ' 0.0.1' ) ).to.equal( '0.0.1' ); - expect( filter( '0.0.1 ' ) ).to.equal( '0.0.1' ); - expect( filter( ' 0.0.1 ' ) ).to.equal( '0.0.1' ); - } ); - } ); - - it( 'validates the provided version (disableInternalVersion=false)', () => { - return cli.provideVersion( '1.0.0', 'major' ) - .then( () => { - const { validate } = questionItems[ 0 ]; - - expect( validate( 'skip' ) ).to.equal( true ); - expect( validate( 'internal' ) ).to.equal( true ); - expect( validate( '2.0.0' ) ).to.equal( true ); - expect( validate( '0.1' ) ).to.equal( 'Please provide a valid version.' ); - } ); - } ); - - it( 'validates the provided version (disableInternalVersion=true)', () => { - return cli.provideVersion( '1.0.0', 'major', { disableInternalVersion: true } ) - .then( () => { - const { validate } = questionItems[ 0 ]; - - expect( validate( 'skip' ) ).to.equal( true ); - expect( validate( 'internal' ) ).to.equal( 'Please provide a valid version.' ); - expect( validate( '2.0.0' ) ).to.equal( true ); - expect( validate( '0.1' ) ).to.equal( 'Please provide a valid version.' ); - } ); - } ); - } ); - - describe( 'provideNewVersionForMonoRepository()', () => { - it( 'bumps major version', () => { - return cli.provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo', 'major' ) - .then( newVersion => { - expect( newVersion ).to.equal( '2.0.0' ); - } ); - } ); - - it( 'bumps minor version', () => { - return cli.provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo', 'minor' ) - .then( newVersion => { - expect( newVersion ).to.equal( '1.1.0' ); - } ); - } ); - - it( 'bumps patch version', () => { - return cli.provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo', 'patch' ) - .then( newVersion => { - expect( newVersion ).to.equal( '1.0.1' ); - } ); - } ); - - it( 'suggest new version', () => { - return cli.provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo', 'minor' ) - .then( () => { - expect( questionItems[ 0 ].default ).to.equal( '1.1.0' ); - } ); - } ); - - it( 'removes spaces from provided version', () => { - return cli.provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo' ) - .then( () => { - const { filter } = questionItems[ 0 ]; - - expect( filter( ' 0.0.1' ) ).to.equal( '0.0.1' ); - expect( filter( '0.0.1 ' ) ).to.equal( '0.0.1' ); - expect( filter( ' 0.0.1 ' ) ).to.equal( '0.0.1' ); - } ); - } ); - - it( 'validates the provided version', () => { - return cli.provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo' ) - .then( () => { - const { validate } = questionItems[ 0 ]; - - expect( validate( '2.0.0' ) ).to.equal( true ); - expect( validate( '1.1.0' ) ).to.equal( true ); - expect( validate( '1.0.0' ) ).to.equal( 'Provided version must be higher than "1.0.0".' ); - expect( validate( 'skip' ) ).to.equal( 'Please provide a valid version.' ); - expect( validate( 'internal' ) ).to.equal( 'Please provide a valid version.' ); - expect( validate( '0.1' ) ).to.equal( 'Please provide a valid version.' ); - } ); - } ); - } ); - - describe( 'provideToken()', () => { - it( 'user is able to provide the token', () => { - return cli.provideToken() - .then( () => { - const question = questionItems[ 0 ]; - - expect( question.message ).to.match( /^Provide the GitHub token/ ); - expect( question.type ).to.equal( 'password' ); - } ); - } ); - - it( 'token must contain 40 characters', () => { - return cli.provideToken() - .then( () => { - const { validate } = questionItems[ 0 ]; - - expect( validate( 'abc' ) ).to.equal( 'Please provide a valid token.' ); - expect( validate( 'a'.repeat( 40 ) ) ).to.equal( true ); - } ); - } ); - } ); - - describe( 'configureReleaseOptions()', () => { - it( 'by default returns both services and requires Github token', () => { - sandbox.stub( cli, 'provideToken' ).resolves( 'a'.repeat( 40 ) ); - - return cli.configureReleaseOptions() - .then( options => { - const question = questionItems[ 0 ]; - - expect( question.message ).to.match( /^Select services where packages will be released:/ ); - expect( question.type ).to.equal( 'checkbox' ); - - expect( cli.provideToken.calledOnce ).to.equal( true ); - - expect( options ).to.deep.equal( { - npm: true, - github: true, - token: 'a'.repeat( 40 ) - } ); - } ); - } ); - - it( 'does not ask about the GitHub token if ignores GitHub release', () => { - sandbox.stub( cli, 'provideToken' ); - userAnswer = [ 'npm' ]; - - return cli.configureReleaseOptions() - .then( options => { - expect( cli.provideToken.called ).to.equal( false ); - - expect( options ).to.deep.equal( { - npm: true, - github: false - } ); - } ); - } ); - } ); - - describe( 'confirmNpmTag()', () => { - it( 'should ask user if continue the release process when passing the same versions', () => { - return cli.confirmNpmTag( 'latest', 'latest' ) - .then( () => { - const question = questionItems[ 0 ]; - - expect( question.message ).to.equal( - 'The next release bumps the "latest" version. Should it be published to npm as "latest"?' - ); - expect( question.type ).to.equal( 'confirm' ); - expect( question.default ).to.equal( true ); - expect( stub.chalk.magenta.callCount ).to.equal( 2 ); - } ); - } ); - - it( 'should ask user if continue the release process when passing different versions', () => { - return cli.confirmNpmTag( 'latest', 'alpha' ) - .then( () => { - const question = questionItems[ 0 ]; - - expect( question.message ).to.equal( - 'The next release bumps the "latest" version. Should it be published to npm as "alpha"?' - ); - expect( question.type ).to.equal( 'confirm' ); - expect( question.default ).to.equal( false ); - expect( stub.chalk.red.callCount ).to.equal( 2 ); - } ); - } ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/configurereleaseoptions.js b/packages/ckeditor5-dev-release-tools/tests/utils/configurereleaseoptions.js new file mode 100644 index 000000000..467e222dc --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/configurereleaseoptions.js @@ -0,0 +1,52 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import inquirer from 'inquirer'; +import provideToken from '../../lib/utils/providetoken.js'; +import configureReleaseOptions from '../../lib/utils/configurereleaseoptions.js'; + +vi.mock( 'inquirer' ); +vi.mock( '../../lib/utils/providetoken.js' ); + +describe( 'configureReleaseOptions()', () => { + it( 'returns npm and Github services and asks for a GitHub token', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { + services: [ 'npm', 'GitHub' ] + } ); + vi.mocked( provideToken ).mockReturnValue( 'a'.repeat( 40 ) ); + + const options = await configureReleaseOptions(); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'services', + type: 'checkbox', + message: 'Select services where packages will be released:', + choices: expect.any( Array ), + default: expect.any( Array ) + } ) + ] ) + ); + + expect( vi.mocked( provideToken ) ).toHaveBeenCalledOnce(); + + expect( options ).toStrictEqual( { + npm: true, + github: true, + token: 'a'.repeat( 40 ) + } ); + } ); + + it( 'should not ask about a GitHub token if processing an npm release only', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { + services: [ 'npm' ] + } ); + + await configureReleaseOptions(); + expect( vi.mocked( provideToken ) ).not.toHaveBeenCalledOnce(); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/confirmincludingpackage.js b/packages/ckeditor5-dev-release-tools/tests/utils/confirmincludingpackage.js new file mode 100644 index 000000000..d63bae99b --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/confirmincludingpackage.js @@ -0,0 +1,31 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import inquirer from 'inquirer'; +import confirmIncludingPackage from '../../lib/utils/confirmincludingpackage.js'; + +vi.mock( 'inquirer' ); + +describe( 'confirmIncludingPackage()', () => { + it( 'user can disagree with the proposed value', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { + confirm: true + } ); + + await expect( confirmIncludingPackage() ).resolves.toBe( true ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'confirm', + type: 'confirm', + message: expect.stringContaining( 'Package does not contain all required files to publish.' ), + default: expect.any( Boolean ) + } ) + ] ) + ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/confirmnpmtag.js b/packages/ckeditor5-dev-release-tools/tests/utils/confirmnpmtag.js new file mode 100644 index 000000000..5b1fd9272 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/confirmnpmtag.js @@ -0,0 +1,61 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import confirmNpmTag from '../../lib/utils/confirmnpmtag.js'; + +vi.mock( 'inquirer' ); +vi.mock( 'chalk', () => ( { + default: { + magenta: vi.fn( input => input ), + red: vi.fn( input => input ) + } +} ) ); + +describe( 'confirmNpmTag()', () => { + it( 'should ask user if continue the release process when passing the same versions', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { + confirm: true + } ); + + await expect( confirmNpmTag( 'latest', 'latest' ) ).resolves.toBe( true ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'confirm', + type: 'confirm', + message: 'The next release bumps the "latest" version. Should it be published to npm as "latest"?', + default: true + } ) + ] ) + ); + + expect( vi.mocked( chalk ).magenta ).toHaveBeenCalledTimes( 2 ); + } ); + + it( 'should ask user if continue the release process when passing different versions', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { + confirm: false + } ); + + await expect( confirmNpmTag( 'latest', 'alpha' ) ).resolves.toBe( false ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'confirm', + type: 'confirm', + message: 'The next release bumps the "latest" version. Should it be published to npm as "alpha"?', + default: false + } ) + ] ) + ); + + expect( vi.mocked( chalk ).red ).toHaveBeenCalledTimes( 2 ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/constants.js b/packages/ckeditor5-dev-release-tools/tests/utils/constants.js new file mode 100644 index 000000000..3e0d6fd67 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/constants.js @@ -0,0 +1,29 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it } from 'vitest'; +import * as constants from '../../lib/utils/constants.js'; + +describe( 'constants', () => { + it( '#CHANGELOG_FILE', async () => { + expect( constants.CHANGELOG_FILE ).to.be.a( 'string' ); + } ); + + it( '#CHANGELOG_HEADER', async () => { + expect( constants.CHANGELOG_HEADER ).to.be.a( 'string' ); + } ); + it( '#CLI_INDENT_SIZE', async () => { + expect( constants.CLI_INDENT_SIZE ).to.be.a( 'number' ); + } ); + + it( '#CLI_COMMIT_INDENT_SIZE', async () => { + expect( constants.CLI_COMMIT_INDENT_SIZE ).to.be.a( 'number' ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/displaycommits.js b/packages/ckeditor5-dev-release-tools/tests/utils/displaycommits.js index c7b800396..a884622db 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/displaycommits.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/displaycommits.js @@ -3,630 +3,696 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi } from 'vitest'; +import displayCommits from '../../lib/utils/displaycommits.js'; + +const stubs = vi.hoisted( () => { + const values = { + logger: { + info: vi.fn() + }, + chalk: { + bold: vi.fn( input => input ), + italic: vi.fn( input => input ), + underline: vi.fn( input => input ), + gray: vi.fn( input => input ), + green: vi.fn( input => input ), + yellow: vi.fn( input => input ), + red: vi.fn( input => input ) + } + }; + + // To make `chalk.bold.yellow.red()` working. + for ( const rootKey of Object.keys( values.chalk ) ) { + for ( const nestedKey of Object.keys( values.chalk ) ) { + values.chalk[ rootKey ][ nestedKey ] = values.chalk[ nestedKey ]; + } + } + + return values; +} ); + +vi.mock( 'chalk', () => ( { + default: stubs.chalk +} ) ); +vi.mock( '@ckeditor/ckeditor5-dev-utils', () => ( { + logger: vi.fn( () => stubs.logger ) +} ) ); + +describe( 'displayCommits()', () => { + it( 'prints if there is no commit to display', () => { + displayCommits( [] ); -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); -describe( 'dev-release-tools/utils', () => { - let displayCommits, sandbox, stubs; + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; - beforeEach( () => { - sandbox = sinon.createSandbox(); + expect( firstArgument ).toContain( 'No commits to display.' ); + } ); - stubs = { - logger: { - info: sandbox.spy(), - warning: sandbox.spy(), - error: sandbox.spy() - } + it( 'attaches valid "external" commit to the changelog (as Array)', () => { + const commit = { + hash: '684997d', + header: 'Fix: Simple fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes: [] }; - displayCommits = proxyquire( '../../lib/utils/displaycommits', { - '@ckeditor/ckeditor5-dev-utils': { - logger() { - return stubs.logger; - } - } - } ); + displayCommits( [ commit ] ); + + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); + + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + + expect( firstArgument ).toContain( 'Fix: Simple fix.' ); + expect( firstArgument ).toContain( 'INCLUDED' ); } ); - afterEach( () => { - sandbox.restore(); + it( 'attaches valid "external" commit to the changelog (as Set)', () => { + const commit = { + hash: '684997d', + header: 'Fix: Simple fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes: [] + }; + + displayCommits( new Set( [ commit ] ) ); + + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); + + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + + expect( firstArgument ).toContain( 'Fix: Simple fix.' ); + expect( firstArgument ).toContain( 'INCLUDED' ); } ); - describe( 'displayCommits()', () => { - it( 'prints if there is no commit to display', () => { - displayCommits( [] ); + it( 'truncates too long commit\'s subject', () => { + const commit = { + hash: '684997d', + header: 'Fix: Reference site about Lorem Ipsum, giving information on its origins, as well as ' + + 'a random Lipsum generator.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Reference site about Lorem Ipsum, giving information on its origins, as well as ' + + 'a random Lipsum generator.', + body: null, + footer: null, + notes: [] + }; - expect( stubs.logger.info.calledOnce ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ] ).includes( 'No commits to display.' ); - } ); + displayCommits( [ commit ] ); - it( 'attaches valid "external" commit to the changelog (as Array)', () => { - const commit = { - hash: '684997d', - header: 'Fix: Simple fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [] - }; + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); - displayCommits( [ commit ] ); + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; - expect( stubs.logger.info.calledOnce ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( 'Fix: Simple fix.' ) ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( 'INCLUDED' ) ).to.equal( true ); - } ); + expect( firstArgument ).toContain( + 'Fix: Reference site about Lorem Ipsum, giving information on its origins, as well as a random Lip...' + ); + expect( firstArgument ).toContain( 'INCLUDED' ); + } ); + + it( 'does not attach valid "internal" commit to the changelog', () => { + const commit = { + hash: '684997d', + header: 'Docs: README.', + type: 'Docs', + rawType: 'Docs', + subject: 'README.', + body: null, + footer: null, + notes: [] + }; + + displayCommits( [ commit ] ); + + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); + + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + + expect( firstArgument ).toContain( 'Docs: README.' ); + expect( firstArgument ).toContain( 'SKIPPED' ); + } ); + + it( 'does not attach invalid commit to the changelog', () => { + const commit = { + hash: '684997d', + header: 'Invalid commit.', + type: null, + subject: null, + body: null, + footer: null, + notes: [] + }; + + displayCommits( [ commit ] ); + + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); + + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + + expect( firstArgument ).toContain( 'Invalid commit.' ); + expect( firstArgument ).toContain( 'INVALID' ); + } ); + + it( 'attaches additional subject for merge commits to the commit list', () => { + const commit = { + merge: 'Merge pull request #75 from ckeditor/t/64', + hash: 'dea3501', + header: 'Feature: Introduced a brand new release tools with a new set of requirements.', + type: 'Feature', + rawType: 'Feature', + subject: 'Introduced a brand new release tools with a new set of requirements.', + body: null, + footer: null, + mentions: [], + notes: [] + }; + + displayCommits( [ commit ] ); + + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); - it( 'attaches valid "external" commit to the changelog (as Set)', () => { + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + const logMessageAsArray = firstArgument.split( '\n' ); + + expect( logMessageAsArray[ 0 ] ).toContain( 'Feature: Introduced a brand new release tools with a new set of requirements.' ); + expect( logMessageAsArray[ 0 ] ).toContain( 'INCLUDED' ); + expect( logMessageAsArray[ 1 ] ).toContain( 'Merge pull request #75 from ckeditor/t/64' ); + } ); + + it( 'displays proper log if commit does not contain the second line', () => { + const commit = { + type: null, + subject: null, + merge: 'Merge branch \'master\' of github.com:ckeditor/ckeditor5-dev', + header: 'Merge branch \'master\' of github.com:ckeditor/ckeditor5-dev', + body: null, + footer: null, + notes: [], + references: [], + mentions: [], + revert: null, + rawType: undefined, + files: [], + scope: undefined, + isPublicCommit: false, + hash: 'a'.repeat( 40 ), + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' + }; + + displayCommits( [ commit ] ); + + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + + // The merge commit displays two lines: + // Prefix: Changes. + // Merge ... + // If the merge commit does not contain the second line, it should display only the one. + expect( firstArgument.split( '\n' ) ).toHaveLength( 1 ); + } ); + + it( 'attaches breaking changes notes to displayed message', () => { + const commit = { + hash: '684997d', + header: 'Feature: Simple foo.', + type: 'Feature', + rawType: 'Feature', + subject: 'Simple foo.', + body: null, + footer: null, + notes: [ + { + title: 'MAJOR BREAKING CHANGES', + text: '1 - Reference site about Lorem Ipsum, giving information on its origins, as well as ' + + 'a random Lipsum generator.' + }, + { + title: 'MAJOR BREAKING CHANGES', + text: '2 - Reference site about Lorem Ipsum, giving information on its origins, as well as ' + + 'a random Lipsum generator.' + }, + { + title: 'MINOR BREAKING CHANGES', + text: '3 - Reference site about Lorem Ipsum, giving information on its origins, as well as ' + + 'a random Lipsum generator.' + } + ] + }; + + displayCommits( [ commit ] ); + + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + const message = firstArgument.split( '\n' ); + + /* eslint-disable max-len */ + expect( message[ 0 ] ).toContain( 'Feature: Simple foo.' ); + expect( message[ 1 ] ).toContain( 'MAJOR BREAKING CHANGES: 1 - Reference site about Lorem Ipsum, giving information on its origins, as...' ); + expect( message[ 2 ] ).toContain( 'MAJOR BREAKING CHANGES: 2 - Reference site about Lorem Ipsum, giving information on its origins, as...' ); + expect( message[ 3 ] ).toContain( 'MINOR BREAKING CHANGES: 3 - Reference site about Lorem Ipsum, giving information on its origins, as...' ); + /* eslint-enable max-len */ + } ); + + describe( 'options.attachLinkToCommit', () => { + it( 'adds a link to displayed commit', () => { const commit = { hash: '684997d', header: 'Fix: Simple fix.', type: 'Bug fixes', - rawType: 'Fix', subject: 'Simple fix.', body: null, footer: null, - notes: [] + notes: [], + rawType: 'Fix', + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-foo' }; - displayCommits( new Set( [ commit ] ) ); + displayCommits( [ commit ], { attachLinkToCommit: true } ); - expect( stubs.logger.info.calledOnce ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( 'Fix: Simple fix.' ) ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( 'INCLUDED' ) ).to.equal( true ); - } ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); - it( 'truncates too long commit\'s subject', () => { - const commit = { - hash: '684997d', - header: 'Fix: Reference site about Lorem Ipsum, giving information on its origins, as well as ' + - 'a random Lipsum generator.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Reference site about Lorem Ipsum, giving information on its origins, as well as ' + - 'a random Lipsum generator.', - body: null, - footer: null, - notes: [] - }; - - displayCommits( [ commit ] ); + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + const logMessage = firstArgument.split( '\n' ); - expect( stubs.logger.info.calledOnce ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( - 'Fix: Reference site about Lorem Ipsum, giving information on its origins, as well as a random Lip...' - ) ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( 'INCLUDED' ) ).to.equal( true ); + expect( logMessage[ 0 ] ).toContain( 'Fix: Simple fix.' ); + expect( logMessage[ 0 ] ).toContain( 'INCLUDED' ); + expect( logMessage[ 1 ] ).toContain( 'https://github.com/ckeditor/ckeditor5-foo/commit/684997d' ); } ); + } ); - it( 'does not attach valid "internal" commit to the changelog', () => { + describe( 'options.indentLevel', () => { + it( 'is equal to 1 by default', () => { const commit = { hash: '684997d', - header: 'Docs: README.', - type: 'Docs', - rawType: 'Docs', - subject: 'README.', + header: 'Fix: Simple fix.', + type: 'Bug fixes', + subject: 'Simple fix.', body: null, footer: null, - notes: [] + notes: [], + rawType: 'Fix' }; displayCommits( [ commit ] ); - expect( stubs.logger.info.calledOnce ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( 'Docs: README.' ) ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( 'SKIPPED' ) ).to.equal( true ); - } ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); - it( 'does not attach invalid commit to the changelog', () => { - const commit = { - hash: '684997d', - header: 'Invalid commit.', - type: null, - subject: null, - body: null, - footer: null, - notes: [] - }; + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; - displayCommits( [ commit ] ); - - expect( stubs.logger.info.calledOnce ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( 'Invalid commit.' ) ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ].includes( 'INVALID' ) ).to.equal( true ); + expect( firstArgument.substring( 0, 3 ) ).toEqual( ' ' ); } ); - it( 'attaches additional subject for merge commits to the commit list', () => { + it( 'indents second line properly', () => { const commit = { + hash: '684997d', merge: 'Merge pull request #75 from ckeditor/t/64', - hash: 'dea3501', header: 'Feature: Introduced a brand new release tools with a new set of requirements.', type: 'Feature', - rawType: 'Feature', subject: 'Introduced a brand new release tools with a new set of requirements.', body: null, footer: null, - mentions: [], - notes: [] + notes: [], + rawType: 'Fix' }; displayCommits( [ commit ] ); - expect( stubs.logger.info.calledOnce ).to.equal( true ); - expect( stubs.logger.info.firstCall.args[ 0 ] ).to.be.a( 'string' ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); - const logMessageAsArray = stubs.logger.info.firstCall.args[ 0 ].split( '\n' ); + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + const [ firstLine, secondLine ] = firstArgument.split( '\n' ); - expect( logMessageAsArray[ 0 ].includes( - 'Feature: Introduced a brand new release tools with a new set of requirements.' - ) ).to.equal( true ); - expect( logMessageAsArray[ 0 ].includes( 'INCLUDED' ) ).to.equal( true ); - expect( logMessageAsArray[ 1 ].includes( 'Merge pull request #75 from ckeditor/t/64' ) ).to.equal( true ); + expect( firstLine.substring( 0, 3 ) ).toEqual( ' '.repeat( 3 ) ); + expect( secondLine.substring( 0, 13 ) ).toEqual( ' '.repeat( 13 ) ); } ); - it( 'displays proper log if commit does not contain the second line', () => { + it( 'works with "options.attachLinkToCommit"', () => { const commit = { - type: null, - subject: null, - merge: 'Merge branch \'master\' of github.com:ckeditor/ckeditor5-dev', - header: 'Merge branch \'master\' of github.com:ckeditor/ckeditor5-dev', + hash: '684997d', + merge: 'Merge pull request #75 from ckeditor/t/64', + header: 'Feature: Introduced a brand new release tools with a new set of requirements.', + type: 'Feature', + subject: 'Introduced a brand new release tools with a new set of requirements.', body: null, footer: null, notes: [], - references: [], - mentions: [], - revert: null, - rawType: undefined, - files: [], - scope: undefined, - isPublicCommit: false, - hash: 'a'.repeat( 40 ), - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' + rawType: 'Fix', + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-foo' }; - displayCommits( [ commit ] ); + displayCommits( [ commit ], { attachLinkToCommit: true, indentLevel: 2 } ); - // The merge commit displays two lines: - // Prefix: Changes. - // Merge ... - // If the merge commit does not contain the second line, it should display only the one. - expect( stubs.logger.info.firstCall.args[ 0 ].split( '\n' ) ).length( 1 ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 1 ); + + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + const [ firstLine, secondLine, thirdLine ] = firstArgument.split( '\n' ); + + expect( firstLine.substring( 0, 6 ) ).toEqual( ' '.repeat( 6 ) ); + expect( secondLine.substring( 0, 16 ) ).toEqual( ' '.repeat( 16 ) ); + expect( thirdLine.substring( 0, 16 ) ).toEqual( ' '.repeat( 16 ) ); } ); + } ); - it( 'attaches breaking changes notes to displayed message', () => { - const commit = { - hash: '684997d', - header: 'Feature: Simple foo.', - type: 'Feature', - rawType: 'Feature', - subject: 'Simple foo.', - body: null, - footer: null, - notes: [ - { - title: 'MAJOR BREAKING CHANGES', - text: '1 - Reference site about Lorem Ipsum, giving information on its origins, as well as ' + - 'a random Lipsum generator.' - }, - { - title: 'MAJOR BREAKING CHANGES', - text: '2 - Reference site about Lorem Ipsum, giving information on its origins, as well as ' + - 'a random Lipsum generator.' - }, - { - title: 'MINOR BREAKING CHANGES', - text: '3 - Reference site about Lorem Ipsum, giving information on its origins, as well as ' + - 'a random Lipsum generator.' - } - ] - }; + describe( 'grouping commits', () => { + it( 'works for a group of two commits between single commit groups', () => { + // Displayed log: + // + // * aaaaaaa "Fix: Another fix." INCLUDED + // --------------------------------------------- + // |* bbbbbbb "Fix: Simple fix." INCLUDED + // |* bbbbbbb "Feature: A new feature." INCLUDED + // --------------------------------------------- + // * ccccccc "Fix: Another fix." INCLUDED + + displayCommits( [ + { + hash: 'a'.repeat( 40 ), + header: 'Fix: Another fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Another fix.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'b'.repeat( 40 ), + header: 'Fix: Simple fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'b'.repeat( 40 ), + header: 'Feature: A new feature.', + type: 'Features', + rawType: 'Feature', + subject: 'A new feature.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'c'.repeat( 40 ), + header: 'Fix: Another fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Another fix.', + body: null, + footer: null, + notes: [] + } + ] ); - displayCommits( [ commit ] ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 6 ); - const message = stubs.logger.info.firstCall.args[ 0 ].split( '\n' ); + const [ , secondCall, , , fifthCall ] = stubs.logger.info.mock.calls; + const [ secondCallfirstArgument ] = secondCall; + const [ fifthCallfirstArgument ] = fifthCall; - /* eslint-disable max-len */ - expect( message[ 0 ].includes( 'Feature: Simple foo.' ) ).to.equal( true ); - expect( message[ 1 ].includes( 'MAJOR BREAKING CHANGES: 1 - Reference site about Lorem Ipsum, giving information on its origins, as...' ) ).to.equal( true ); - expect( message[ 2 ].includes( 'MAJOR BREAKING CHANGES: 2 - Reference site about Lorem Ipsum, giving information on its origins, as...' ) ).to.equal( true ); - expect( message[ 3 ].includes( 'MINOR BREAKING CHANGES: 3 - Reference site about Lorem Ipsum, giving information on its origins, as...' ) ).to.equal( true ); - /* eslint-enable max-len */ + // Calls: 0, 2, 3, and 5 display the commit data. + expect( secondCallfirstArgument ).toMatch( /-----/ ); + expect( fifthCallfirstArgument ).toMatch( /-----/ ); } ); - describe( 'options.attachLinkToCommit', () => { - it( 'adds a link to displayed commit', () => { - const commit = { - hash: '684997d', + it( 'works for a group of two commits that follows a single commit group', () => { + // Displayed log: + // + // * aaaaaaa "Fix: Another fix." INCLUDED + // --------------------------------------------- + // |* bbbbbbb "Fix: Simple fix." INCLUDED + // |* bbbbbbb "Feature: A new feature." INCLUDED + // --------------------------------------------- + displayCommits( [ + { + hash: 'a'.repeat( 40 ), + header: 'Fix: Another fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Another fix.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'b'.repeat( 40 ), header: 'Fix: Simple fix.', type: 'Bug fixes', + rawType: 'Fix', subject: 'Simple fix.', body: null, footer: null, - notes: [], - rawType: 'Fix', - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-foo' - }; - - displayCommits( [ commit ], { attachLinkToCommit: true } ); + notes: [] + }, + { + hash: 'b'.repeat( 40 ), + header: 'Feature: A new feature.', + type: 'Features', + rawType: 'Feature', + subject: 'A new feature.', + body: null, + footer: null, + notes: [] + } + ] ); - expect( stubs.logger.info.calledOnce ).to.equal( true ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 5 ); - const logMessage = stubs.logger.info.firstCall.args[ 0 ].split( '\n' ); + const [ , secondCall, , , fifthCall ] = stubs.logger.info.mock.calls; + const [ secondCallfirstArgument ] = secondCall; + const [ fifthCallfirstArgument ] = fifthCall; - expect( logMessage[ 0 ].includes( 'Fix: Simple fix.' ) ).to.equal( true ); - expect( logMessage[ 0 ].includes( 'INCLUDED' ) ).to.equal( true ); - expect( logMessage[ 1 ].includes( 'https://github.com/ckeditor/ckeditor5-foo/commit/684997d' ) ).to.equal( true ); - } ); + // Calls: 0, 2, and 3 display the commit data. + expect( secondCallfirstArgument ).toMatch( /-----/ ); + expect( fifthCallfirstArgument ).toMatch( /-----/ ); } ); - describe( 'options.indentLevel', () => { - it( 'is equal to 1 by default', () => { - const commit = { - hash: '684997d', + it( 'works for a single commit group that follows group of two commits ', () => { + // Displayed log: + // + // --------------------------------------------- + // |* bbbbbbb "Fix: Simple fix." INCLUDED + // |* bbbbbbb "Feature: A new feature." INCLUDED + // --------------------------------------------- + // * ccccccc "Fix: Another fix." INCLUDED + + displayCommits( [ + { + hash: 'b'.repeat( 40 ), header: 'Fix: Simple fix.', type: 'Bug fixes', + rawType: 'Fix', subject: 'Simple fix.', body: null, footer: null, - notes: [], - rawType: 'Fix' - }; - - displayCommits( [ commit ] ); + notes: [] + }, + { + hash: 'b'.repeat( 40 ), + header: 'Feature: A new feature.', + type: 'Features', + rawType: 'Feature', + subject: 'A new feature.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'c'.repeat( 40 ), + header: 'Fix: Another fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Another fix.', + body: null, + footer: null, + notes: [] + } + ] ); - expect( stubs.logger.info.calledOnce ).to.equal( true ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 5 ); - const logMessage = stubs.logger.info.firstCall.args[ 0 ]; + const [ firstCall, , , fourthCall ] = stubs.logger.info.mock.calls; + const [ firstCallfirstArgument ] = firstCall; + const [ fourthCallfirstArgument ] = fourthCall; - expect( logMessage.substring( 0, 3 ) ).to.equal( ' ' ); - } ); + // Calls: 1, 2, and 4 display the commit data. + expect( firstCallfirstArgument ).toMatch( /-----/ ); + expect( fourthCallfirstArgument ).toMatch( /-----/ ); + } ); - it( 'indents second line properly', () => { - const commit = { - hash: '684997d', - merge: 'Merge pull request #75 from ckeditor/t/64', - header: 'Feature: Introduced a brand new release tools with a new set of requirements.', - type: 'Feature', - subject: 'Introduced a brand new release tools with a new set of requirements.', + it( 'does not duplicate the separator for commit groups', () => { + // Displayed log: + // + // --------------------------------------------- + // |* bbbbbbb "Fix: Simple fix." INCLUDED + // |* bbbbbbb "Feature: A new feature." INCLUDED + // --------------------------------------------- + // |* ccccccc "Fix: One Another fix." INCLUDED + // |* ccccccc "Fix: Another fix." INCLUDED + // --------------------------------------------- + + displayCommits( [ + { + hash: 'b'.repeat( 40 ), + header: 'Fix: Simple fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Simple fix.', body: null, footer: null, - notes: [], - rawType: 'Fix' - }; - - displayCommits( [ commit ] ); + notes: [] + }, + { + hash: 'b'.repeat( 40 ), + header: 'Feature: A new feature.', + type: 'Features', + rawType: 'Feature', + subject: 'A new feature.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'c'.repeat( 40 ), + header: 'Fix: One Another fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'One Another fix.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'c'.repeat( 40 ), + header: 'Fix: Another fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Another fix.', + body: null, + footer: null, + notes: [] + } + ] ); - expect( stubs.logger.info.calledOnce ).to.equal( true ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 7 ); - const [ firstLine, secondLine ] = stubs.logger.info.firstCall.args[ 0 ].split( '\n' ); + const [ firstCall, , , fourthCall, fifthCall, , seventhCall ] = stubs.logger.info.mock.calls; + const [ firstCallfirstArgument ] = firstCall; + const [ fourthCallfirstArgument ] = fourthCall; + const [ fifthCallfirstArgument ] = fifthCall; + const [ seventhCallfirstArgument ] = seventhCall; - expect( firstLine.substring( 0, 3 ) ).to.equal( ' '.repeat( 3 ) ); - expect( secondLine.substring( 0, 13 ) ).to.equal( ' '.repeat( 13 ) ); - } ); + // Calls: 1, 2, 4, and 5 display the commit data. + expect( firstCallfirstArgument ).toMatch( /-----/ ); + expect( fourthCallfirstArgument ).toMatch( /-----/ ); + expect( fifthCallfirstArgument ).not.toMatch( /-----/ ); + expect( seventhCallfirstArgument ).toMatch( /-----/ ); + } ); - it( 'works with "options.attachLinkToCommit"', () => { - const commit = { - hash: '684997d', - merge: 'Merge pull request #75 from ckeditor/t/64', - header: 'Feature: Introduced a brand new release tools with a new set of requirements.', - type: 'Feature', - subject: 'Introduced a brand new release tools with a new set of requirements.', + it( 'groups two groups of commits separated by a single commit group', () => { + // Displayed log: + // + // --------------------------------------------- + // |* bbbbbbb "Fix: Simple fix." INCLUDED + // |* bbbbbbb "Feature: A new feature." INCLUDED + // --------------------------------------------- + // * aaaaaaa "Fix: Another fix." INCLUDED + // --------------------------------------------- + // |* ccccccc "Fix: One Another fix." INCLUDED + // |* ccccccc "Fix: Another fix." INCLUDED + // --------------------------------------------- + + displayCommits( [ + { + hash: 'b'.repeat( 40 ), + header: 'Fix: Simple fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Simple fix.', body: null, footer: null, - notes: [], + notes: [] + }, + { + hash: 'b'.repeat( 40 ), + header: 'Feature: A new feature.', + type: 'Features', + rawType: 'Feature', + subject: 'A new feature.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'a'.repeat( 40 ), + header: 'Fix: Another fix.', + type: 'Bug fixes', rawType: 'Fix', - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-foo' - }; - - displayCommits( [ commit ], { attachLinkToCommit: true, indentLevel: 2 } ); + subject: 'Another fix.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'c'.repeat( 40 ), + header: 'Fix: One Another fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'One Another fix.', + body: null, + footer: null, + notes: [] + }, + { + hash: 'c'.repeat( 40 ), + header: 'Fix: Another fix.', + type: 'Bug fixes', + rawType: 'Fix', + subject: 'Another fix.', + body: null, + footer: null, + notes: [] + } + ] ); - expect( stubs.logger.info.calledOnce ).to.equal( true ); + expect( stubs.logger.info ).toHaveBeenCalledTimes( 9 ); - const [ firstLine, secondLine, thirdLine ] = stubs.logger.info.firstCall.args[ 0 ].split( '\n' ); + const [ firstCall, , , fourthCall, , sixthCall, , , ninethCall ] = stubs.logger.info.mock.calls; + const [ firstCallfirstArgument ] = firstCall; + const [ fourthCallfirstArgument ] = fourthCall; + const [ sixthCallfirstArgument ] = sixthCall; + const [ ninethCallfirstArgument ] = ninethCall; - expect( firstLine.substring( 0, 6 ) ).to.equal( ' '.repeat( 6 ) ); - expect( secondLine.substring( 0, 16 ) ).to.equal( ' '.repeat( 16 ) ); - expect( thirdLine.substring( 0, 16 ) ).to.equal( ' '.repeat( 16 ) ); - } ); - } ); + // Calls: 1, 2, 4, 6, and 7 display the commit data. + expect( firstCallfirstArgument ).to.match( /-----/ ); + expect( fourthCallfirstArgument ).to.match( /-----/ ); - describe( 'grouping commits', () => { - it( 'works for a group of two commits between single commit groups', () => { - // Displayed log: - // - // * aaaaaaa "Fix: Another fix." INCLUDED - // --------------------------------------------- - // |* bbbbbbb "Fix: Simple fix." INCLUDED - // |* bbbbbbb "Feature: A new feature." INCLUDED - // --------------------------------------------- - // * ccccccc "Fix: Another fix." INCLUDED - - displayCommits( [ - { - hash: 'a'.repeat( 40 ), - header: 'Fix: Another fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Another fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'b'.repeat( 40 ), - header: 'Fix: Simple fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'b'.repeat( 40 ), - header: 'Feature: A new feature.', - type: 'Features', - rawType: 'Feature', - subject: 'A new feature.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'c'.repeat( 40 ), - header: 'Fix: Another fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Another fix.', - body: null, - footer: null, - notes: [] - } - ] ); - - // Calls: 0, 2, 3, and 5 display the commit data. - expect( stubs.logger.info.callCount ).to.equal( 6 ); - expect( stubs.logger.info.getCall( 1 ).args[ 0 ] ).to.match( /-----/ ); - expect( stubs.logger.info.getCall( 4 ).args[ 0 ] ).to.match( /-----/ ); - } ); - - it( 'works for a group of two commits that follows a single commit group', () => { - // Displayed log: - // - // * aaaaaaa "Fix: Another fix." INCLUDED - // --------------------------------------------- - // |* bbbbbbb "Fix: Simple fix." INCLUDED - // |* bbbbbbb "Feature: A new feature." INCLUDED - // --------------------------------------------- - displayCommits( [ - { - hash: 'a'.repeat( 40 ), - header: 'Fix: Another fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Another fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'b'.repeat( 40 ), - header: 'Fix: Simple fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'b'.repeat( 40 ), - header: 'Feature: A new feature.', - type: 'Features', - rawType: 'Feature', - subject: 'A new feature.', - body: null, - footer: null, - notes: [] - } - ] ); - - // Calls: 0, 2, and 3 display the commit data. - expect( stubs.logger.info.callCount ).to.equal( 5 ); - expect( stubs.logger.info.getCall( 1 ).args[ 0 ] ).to.match( /-----/ ); - expect( stubs.logger.info.getCall( 4 ).args[ 0 ] ).to.match( /-----/ ); - } ); - - it( 'works for a single commit group that follows group of two commits ', () => { - // Displayed log: - // - // --------------------------------------------- - // |* bbbbbbb "Fix: Simple fix." INCLUDED - // |* bbbbbbb "Feature: A new feature." INCLUDED - // --------------------------------------------- - // * ccccccc "Fix: Another fix." INCLUDED - - displayCommits( [ - { - hash: 'b'.repeat( 40 ), - header: 'Fix: Simple fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'b'.repeat( 40 ), - header: 'Feature: A new feature.', - type: 'Features', - rawType: 'Feature', - subject: 'A new feature.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'c'.repeat( 40 ), - header: 'Fix: Another fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Another fix.', - body: null, - footer: null, - notes: [] - } - ] ); - - // Calls: 1, 2, and 4 display the commit data. - expect( stubs.logger.info.callCount ).to.equal( 5 ); - expect( stubs.logger.info.getCall( 0 ).args[ 0 ] ).to.match( /-----/ ); - expect( stubs.logger.info.getCall( 3 ).args[ 0 ] ).to.match( /-----/ ); - } ); - - it( 'does not duplicate the separator for commit groups', () => { - // Displayed log: - // - // --------------------------------------------- - // |* bbbbbbb "Fix: Simple fix." INCLUDED - // |* bbbbbbb "Feature: A new feature." INCLUDED - // --------------------------------------------- - // |* ccccccc "Fix: One Another fix." INCLUDED - // |* ccccccc "Fix: Another fix." INCLUDED - // --------------------------------------------- - - displayCommits( [ - { - hash: 'b'.repeat( 40 ), - header: 'Fix: Simple fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'b'.repeat( 40 ), - header: 'Feature: A new feature.', - type: 'Features', - rawType: 'Feature', - subject: 'A new feature.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'c'.repeat( 40 ), - header: 'Fix: One Another fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'One Another fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'c'.repeat( 40 ), - header: 'Fix: Another fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Another fix.', - body: null, - footer: null, - notes: [] - } - ] ); - - // Calls: 1, 2, 4, and 5 display the commit data. - expect( stubs.logger.info.callCount ).to.equal( 7 ); - expect( stubs.logger.info.getCall( 0 ).args[ 0 ] ).to.match( /-----/ ); - expect( stubs.logger.info.getCall( 3 ).args[ 0 ] ).to.match( /-----/ ); - expect( stubs.logger.info.getCall( 4 ).args[ 0 ] ).to.not.match( /-----/ ); - expect( stubs.logger.info.getCall( 6 ).args[ 0 ] ).to.match( /-----/ ); - } ); - - it( 'groups two groups of commits separated by a single commit group', () => { - // Displayed log: - // - // --------------------------------------------- - // |* bbbbbbb "Fix: Simple fix." INCLUDED - // |* bbbbbbb "Feature: A new feature." INCLUDED - // --------------------------------------------- - // * aaaaaaa "Fix: Another fix." INCLUDED - // --------------------------------------------- - // |* ccccccc "Fix: One Another fix." INCLUDED - // |* ccccccc "Fix: Another fix." INCLUDED - // --------------------------------------------- - - displayCommits( [ - { - hash: 'b'.repeat( 40 ), - header: 'Fix: Simple fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'b'.repeat( 40 ), - header: 'Feature: A new feature.', - type: 'Features', - rawType: 'Feature', - subject: 'A new feature.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'a'.repeat( 40 ), - header: 'Fix: Another fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Another fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'c'.repeat( 40 ), - header: 'Fix: One Another fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'One Another fix.', - body: null, - footer: null, - notes: [] - }, - { - hash: 'c'.repeat( 40 ), - header: 'Fix: Another fix.', - type: 'Bug fixes', - rawType: 'Fix', - subject: 'Another fix.', - body: null, - footer: null, - notes: [] - } - ] ); - - // Calls: 1, 2, 4, 6, and 7 display the commit data. - expect( stubs.logger.info.callCount ).to.equal( 9 ); - expect( stubs.logger.info.getCall( 0 ).args[ 0 ] ).to.match( /-----/ ); - expect( stubs.logger.info.getCall( 3 ).args[ 0 ] ).to.match( /-----/ ); - - expect( stubs.logger.info.getCall( 5 ).args[ 0 ] ).to.match( /-----/ ); - expect( stubs.logger.info.getCall( 8 ).args[ 0 ] ).to.match( /-----/ ); - } ); + expect( sixthCallfirstArgument ).to.match( /-----/ ); + expect( ninethCallfirstArgument ).to.match( /-----/ ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/displayskippedpackages.js b/packages/ckeditor5-dev-release-tools/tests/utils/displayskippedpackages.js index 523c4ddf0..b15ca9d06 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/displayskippedpackages.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/displayskippedpackages.js @@ -3,63 +3,71 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi } from 'vitest'; +import chalk from 'chalk'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import getPackageJson from '../../lib/utils/getpackagejson.js'; +import displaySkippedPackages from '../../lib/utils/displayskippedpackages.js'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); +const stubs = vi.hoisted( () => { + const values = { + logger: { + info: vi.fn() + }, + chalk: { + bold: vi.fn( input => input ), + underline: vi.fn( input => input ) + } + }; -describe( 'dev-release-tools/utils', () => { - let displaySkippedPackages, sandbox, stubs; + // To make `chalk.bold.yellow.red()` working. + for ( const rootKey of Object.keys( values.chalk ) ) { + for ( const nestedKey of Object.keys( values.chalk ) ) { + values.chalk[ rootKey ][ nestedKey ] = values.chalk[ nestedKey ]; + } + } - beforeEach( () => { - sandbox = sinon.createSandbox(); + return values; +} ); - stubs = { - logger: { - info: sandbox.stub(), - warning: sandbox.stub(), - error: sandbox.stub() - }, - getPackageJson: sandbox.stub() - }; +vi.mock( 'chalk', () => ( { + default: stubs.chalk +} ) ); +vi.mock( '@ckeditor/ckeditor5-dev-utils', () => ( { + logger: vi.fn( () => stubs.logger ) +} ) ); +vi.mock( '../../lib/utils/constants.js', () => ( { + CLI_INDENT_SIZE: 1 +} ) ); +vi.mock( '../../lib/utils/getpackagejson.js' ); - displaySkippedPackages = proxyquire( '../../lib/utils/displayskippedpackages', { - '@ckeditor/ckeditor5-dev-utils': { - logger() { - return stubs.logger; - } - }, - './getpackagejson': stubs.getPackageJson - } ); - } ); +describe( 'displaySkippedPackages()', () => { + it( 'displays name of packages that have been skipped', () => { + vi.mocked( getPackageJson ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-foo' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-bar' } ); - afterEach( () => { - sandbox.restore(); - } ); + displaySkippedPackages( new Set( [ + '/packages/ckeditor5-foo', + '/packages/ckeditor5-bar' + ] ) ); - describe( 'displaySkippedPackages()', () => { - it( 'displays name of packages that have been skipped', () => { - stubs.getPackageJson.onFirstCall().returns( { name: '@ckeditor/ckeditor5-foo' } ); - stubs.getPackageJson.onSecondCall().returns( { name: '@ckeditor/ckeditor5-bar' } ); + expect( vi.mocked( logger ) ).toHaveBeenCalledOnce(); + expect( stubs.logger.info ).toHaveBeenCalledOnce(); - displaySkippedPackages( new Set( [ - '/packages/ckeditor5-foo', - '/packages/ckeditor5-bar' - ] ) ); + const [ firstCall ] = stubs.logger.info.mock.calls; + const [ firstArgument ] = firstCall; + const logMessage = firstArgument.split( '\n' ); - expect( stubs.logger.info.calledOnce ).to.equal( true ); + expect( logMessage[ 0 ].includes( 'Packages listed below have been skipped:' ) ).to.equal( true ); + expect( logMessage[ 1 ].includes( ' * @ckeditor/ckeditor5-foo' ) ).to.equal( true ); + expect( logMessage[ 2 ].includes( ' * @ckeditor/ckeditor5-bar' ) ).to.equal( true ); - const logMessage = stubs.logger.info.firstCall.args[ 0 ].split( '\n' ); - - expect( logMessage[ 0 ].includes( 'Packages listed below have been skipped:' ) ).to.equal( true ); - expect( logMessage[ 1 ].includes( ' * @ckeditor/ckeditor5-foo' ) ).to.equal( true ); - expect( logMessage[ 2 ].includes( ' * @ckeditor/ckeditor5-bar' ) ).to.equal( true ); - } ); + expect( vi.mocked( chalk ).underline ).toHaveBeenCalledOnce(); + } ); - it( 'does not display if given list is empty', () => { - displaySkippedPackages( new Set() ); - expect( stubs.logger.info.calledOnce ).to.equal( false ); - } ); + it( 'does not display if given list is empty', () => { + displaySkippedPackages( new Set() ); + expect( stubs.logger.info ).not.toHaveBeenCalledOnce(); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/executeinparallel-integration.js b/packages/ckeditor5-dev-release-tools/tests/utils/executeinparallel-integration.js deleted file mode 100644 index 64c1c5f16..000000000 --- a/packages/ckeditor5-dev-release-tools/tests/utils/executeinparallel-integration.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const expect = require( 'chai' ).expect; -const fs = require( 'fs' ); -const path = require( 'path' ); -const sinon = require( 'sinon' ); -const glob = require( 'glob' ); - -const REPOSITORY_ROOT = path.join( __dirname, '..', '..', '..', '..' ); - -// This file covers the "parallelworker.cjs" file. - -describe( 'dev-release-tools/utils', () => { - let executeInParallel, abortController; - - beforeEach( () => { - abortController = new AbortController(); - executeInParallel = require( '../../lib/utils/executeinparallel' ); - } ); - - afterEach( () => { - sinon.restore(); - } ); - - describe( 'executeInParallel() - integration', () => { - it( 'should store current time in all found packages (callback returns a promise)', async () => { - const timeBefore = new Date().getTime(); - - await executeInParallel( { - cwd: REPOSITORY_ROOT, - concurrency: 2, - packagesDirectory: 'packages', - signal: abortController.signal, - taskToExecute: async packagePath => { - const fs = require( 'fs/promises' ); - const path = require( 'path' ); - const filePath = path.join( packagePath, 'executeinparallel-integration.log' ); - - await fs.writeFile( filePath, new Date().getTime().toString() ); - }, - listrTask: { - output: '' - } - } ); - - const timeAfter = new Date().getTime(); - - const data = glob.sync( 'packages/*/executeinparallel-integration.log', { cwd: REPOSITORY_ROOT, absolute: true } ) - .map( logFile => { - return { - source: logFile, - value: parseInt( fs.readFileSync( logFile, 'utf-8' ) ), - packageName: logFile.split( '/' ).reverse().slice( 1, 2 ).pop() - }; - } ); - - for ( const { value, packageName, source } of data ) { - expect( value > timeBefore, `comparing timeBefore (${ packageName })` ).to.equal( true ); - expect( value < timeAfter, `comparing timeAfter (${ packageName })` ).to.equal( true ); - - fs.unlinkSync( source ); - } - } ); - - it( 'should store current time in all found packages (callback is a synchronous function)', async () => { - const timeBefore = new Date().getTime(); - - await executeInParallel( { - cwd: REPOSITORY_ROOT, - concurrency: 2, - packagesDirectory: 'packages', - signal: abortController.signal, - taskToExecute: packagePath => { - const fs = require( 'fs' ); - const path = require( 'path' ); - const filePath = path.join( packagePath, 'executeinparallel-integration.log' ); - - fs.writeFileSync( filePath, new Date().getTime().toString() ); - }, - listrTask: { - output: '' - } - } ); - - const timeAfter = new Date().getTime(); - - const data = glob.sync( 'packages/*/executeinparallel-integration.log', { cwd: REPOSITORY_ROOT, absolute: true } ) - .map( logFile => { - return { - source: logFile, - value: parseInt( fs.readFileSync( logFile, 'utf-8' ) ), - packageName: logFile.split( '/' ).reverse().slice( 1, 2 ).pop() - }; - } ); - - for ( const { value, packageName, source } of data ) { - expect( value > timeBefore, `comparing timeBefore (${ packageName })` ).to.equal( true ); - expect( value < timeAfter, `comparing timeAfter (${ packageName })` ).to.equal( true ); - - fs.unlinkSync( source ); - } - } ); - - it( 'should pass task options to the worker', async () => { - await executeInParallel( { - cwd: REPOSITORY_ROOT, - concurrency: 2, - packagesDirectory: 'packages', - signal: abortController.signal, - taskToExecute: async ( packagePath, taskOptions ) => { - const fs = require( 'fs/promises' ); - const path = require( 'path' ); - const filePath = path.join( packagePath, 'executeinparallel-integration.log' ); - - await fs.writeFile( filePath, JSON.stringify( taskOptions ) ); - }, - taskOptions: { - property: 'Example of the property.', - some: { - deeply: { - nested: { - property: 'Example the deeply nested property.' - } - } - } - }, - listrTask: { - output: '' - } - } ); - - const data = glob.sync( 'packages/*/executeinparallel-integration.log', { cwd: REPOSITORY_ROOT, absolute: true } ) - .map( logFile => { - return { - source: logFile, - value: JSON.parse( fs.readFileSync( logFile, 'utf-8' ) ), - packageName: logFile.split( '/' ).reverse().slice( 1, 2 ).pop() - }; - } ); - - for ( const { value, packageName, source } of data ) { - expect( value, `comparing taskOptions (${ packageName })` ).to.deep.equal( { - property: 'Example of the property.', - some: { - deeply: { - nested: { - property: 'Example the deeply nested property.' - } - } - } - } ); - - fs.unlinkSync( source ); - } - } ); - } ); -} ); - diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/executeinparallel.js b/packages/ckeditor5-dev-release-tools/tests/utils/executeinparallel.js index 0cdefc38b..2b88c78cb 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/executeinparallel.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/executeinparallel.js @@ -3,70 +3,66 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { glob } from 'glob'; +import fs from 'fs/promises'; +import { registerAbortController, deregisterAbortController } from '../../lib/utils/abortcontroller.js'; +import executeInParallel from '../../lib/utils/executeinparallel.js'; +import os from 'os'; + +const stubs = vi.hoisted( () => ( { + WorkerMock: class { + constructor( script, options ) { + // Define a static property that keeps all instances for a particular test scenario. + if ( !this.constructor.instances ) { + this.constructor.instances = []; + } -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); + this.constructor.instances.push( this ); -describe( 'dev-release-tools/utils', () => { - let executeInParallel, stubs, abortController, WorkerMock, defaultOptions, outputHistory; + this.workerData = options.workerData; + this.on = vi.fn(); + this.terminate = vi.fn(); - beforeEach( () => { - WorkerMock = class { - constructor( script, options ) { - // Define a static property that keeps all instances for a particular test scenario. - if ( !this.constructor.instances ) { - this.constructor.instances = []; - } + expect( script.toString().endsWith( 'parallelworker.js' ) ).toEqual( true ); + } + } +} ) ); - this.constructor.instances.push( this ); +vi.mock( 'worker_threads', () => ( { + Worker: stubs.WorkerMock +} ) ); - this.workerData = options.workerData; - this.on = sinon.stub(); - this.terminate = sinon.stub(); +vi.mock( 'os', () => ( { + default: { + cpus: vi.fn( () => new Array( 4 ) ) + } +} ) ); - expect( script.endsWith( 'parallelworker.cjs' ) ).to.equal( true ); - } - }; +vi.mock( 'crypto', () => ( { + default: { + randomUUID: vi.fn( () => 'uuid-4' ) + } +} ) ); - outputHistory = []; +vi.mock( 'glob' ); +vi.mock( 'fs/promises' ); +vi.mock( '../../lib/utils/abortcontroller.js' ); - stubs = { - process: { - cwd: sinon.stub( process, 'cwd' ).returns( '/home/ckeditor' ) - }, - os: { - cpus: sinon.stub().returns( new Array( 4 ) ) - }, - crypto: { - randomUUID: sinon.stub().returns( 'uuid-4' ) - }, - fs: { - writeFile: sinon.stub().resolves(), - unlink: sinon.stub().resolves() - }, - worker_threads: { - Worker: WorkerMock - }, - glob: { - glob: sinon.stub().resolves( [ - '/home/ckeditor/my-packages/package-01', - '/home/ckeditor/my-packages/package-02', - '/home/ckeditor/my-packages/package-03', - '/home/ckeditor/my-packages/package-04' - ] ) - }, - spinnerStub: { - start: sinon.stub(), - finish: sinon.stub(), - increase: sinon.stub() - }, - abortController: { - registerAbortController: sinon.stub(), - deregisterAbortController: sinon.stub() - } - }; +describe( 'executeInParallel()', () => { + let abortController, defaultOptions, outputHistory; + + beforeEach( () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/home/ckeditor' ); + + vi.mocked( glob ).mockResolvedValue( [ + '/home/ckeditor/my-packages/package-01', + '/home/ckeditor/my-packages/package-02', + '/home/ckeditor/my-packages/package-03', + '/home/ckeditor/my-packages/package-04' + ] ); + + outputHistory = []; abortController = new AbortController(); @@ -80,555 +76,614 @@ describe( 'dev-release-tools/utils', () => { } } }; - - executeInParallel = proxyquire( '../../lib/utils/executeinparallel', { - os: stubs.os, - crypto: stubs.crypto, - 'fs/promises': stubs.fs, - worker_threads: stubs.worker_threads, - glob: stubs.glob, - './abortcontroller': stubs.abortController - } ); } ); afterEach( () => { - sinon.restore(); + // Since the mock is shared across all tests, reset static property that keeps all created instances. + stubs.WorkerMock.instances = []; } ); - describe( 'executeInParallel()', () => { - it( 'should execute the specified `taskToExecute` on all packages found in the `packagesDirectory`', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + it( 'should execute the specified `taskToExecute` on all packages found in the `packagesDirectory`', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - // By default the helper uses a half of available CPUs. - expect( WorkerMock.instances ).to.lengthOf( 2 ); + // By default the helper uses a half of available CPUs. + expect( stubs.WorkerMock.instances ).toHaveLength( 2 ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - expect( stubs.glob.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.equal( 'my-packages/*/' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.be.an( 'object' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'cwd', '/home/ckeditor' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'absolute', true ); + expect( glob ).toHaveBeenCalledTimes( 1 ); + expect( glob ).toHaveBeenCalledWith( 'my-packages/*/', expect.objectContaining( { + cwd: '/home/ckeditor', + absolute: true + } ) ); - expect( stubs.fs.writeFile.callCount ).to.equal( 1 ); - expect( stubs.fs.writeFile.firstCall.args[ 0 ] ).to.equal( '/home/ckeditor/uuid-4.cjs' ); - expect( stubs.fs.writeFile.firstCall.args[ 1 ] ).to.equal( - '\'use strict\';\nmodule.exports = packagePath => console.log( \'pwd\', packagePath );' - ); - expect( firstWorker.workerData ).to.be.an( 'object' ); - expect( firstWorker.workerData ).to.have.property( 'callbackModule', '/home/ckeditor/uuid-4.cjs' ); - expect( firstWorker.workerData ).to.have.property( 'packages' ); + expect( fs.writeFile ).toHaveBeenCalledTimes( 1 ); + expect( fs.writeFile ).toHaveBeenCalledWith( + '/home/ckeditor/uuid-4.mjs', + 'export default packagePath => console.log( \'pwd\', packagePath );', + 'utf-8' + ); + expect( firstWorker.workerData ).toBeInstanceOf( Object ); + expect( firstWorker.workerData ).toHaveProperty( 'callbackModule', '/home/ckeditor/uuid-4.mjs' ); + expect( firstWorker.workerData ).toHaveProperty( 'packages' ); + + expect( secondWorker.workerData ).toBeInstanceOf( Object ); + expect( secondWorker.workerData ).toHaveProperty( 'callbackModule', '/home/ckeditor/uuid-4.mjs' ); + expect( secondWorker.workerData ).toHaveProperty( 'packages' ); - expect( secondWorker.workerData ).to.be.an( 'object' ); - expect( secondWorker.workerData ).to.have.property( 'callbackModule', '/home/ckeditor/uuid-4.cjs' ); - expect( secondWorker.workerData ).to.have.property( 'packages' ); + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + await promise; + } ); - await promise; + it( 'should execute the specified `taskToExecute` on packages found in the `packagesDirectory` that are not filtered', async () => { + const options = Object.assign( {}, defaultOptions, { + // Skip "package-02". + packagesDirectoryFilter: packageDirectory => !packageDirectory.endsWith( 'package-02' ) } ); - it( 'should execute the specified `taskToExecute` on packages found in the `packagesDirectory` that are not filtered', async () => { - const options = Object.assign( {}, defaultOptions, { - // Skip "package-02". - packagesDirectoryFilter: packageDirectory => !packageDirectory.endsWith( 'package-02' ) - } ); + const promise = executeInParallel( options ); + await delay( 0 ); - const promise = executeInParallel( options ); - await delay( 0 ); + // By default the helper uses a half of available CPUs. + expect( stubs.WorkerMock.instances ).toHaveLength( 2 ); - // By default the helper uses a half of available CPUs. - expect( WorkerMock.instances ).to.lengthOf( 2 ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - const [ firstWorker, secondWorker ] = WorkerMock.instances; + expect( firstWorker.workerData.packages ).toEqual( [ + '/home/ckeditor/my-packages/package-01', + '/home/ckeditor/my-packages/package-04' + ] ); - expect( firstWorker.workerData.packages ).to.deep.equal( [ - '/home/ckeditor/my-packages/package-01', - '/home/ckeditor/my-packages/package-04' - ] ); + expect( secondWorker.workerData.packages ).toEqual( [ + '/home/ckeditor/my-packages/package-03' + ] ); - expect( secondWorker.workerData.packages ).to.deep.equal( [ - '/home/ckeditor/my-packages/package-03' - ] ); + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + await promise; + } ); - await promise; + it( 'should use the specified `cwd` when looking for packages', async () => { + const options = Object.assign( {}, defaultOptions, { + cwd: '/custom/cwd' } ); - it( 'should use the specified `cwd` when looking for packages', async () => { - const options = Object.assign( {}, defaultOptions, { - cwd: '/custom/cwd' - } ); + const promise = executeInParallel( options ); + await delay( 0 ); - const promise = executeInParallel( options ); - await delay( 0 ); + expect( glob ).toHaveBeenCalledTimes( 1 ); + expect( glob ).toHaveBeenCalledWith( 'my-packages/*/', expect.objectContaining( { + cwd: '/custom/cwd', + absolute: true + } ) ); - expect( stubs.glob.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.equal( 'my-packages/*/' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.be.an( 'object' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'cwd', '/custom/cwd' ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - const [ firstWorker, secondWorker ] = WorkerMock.instances; + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + await promise; + } ); - await promise; - } ); + it( 'should normalize the current working directory to unix-style (default value, Windows path)', async () => { + process.cwd.mockReturnValue( 'C:\\Users\\ckeditor' ); - it( 'should normalize the current working directory to unix-style (default value, Windows path)', async () => { - stubs.process.cwd.returns( 'C:\\Users\\ckeditor' ); + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + expect( glob ).toHaveBeenCalledTimes( 1 ); + expect( glob ).toHaveBeenCalledWith( 'my-packages/*/', expect.objectContaining( { + cwd: 'C:/Users/ckeditor', + absolute: true + } ) ); - expect( stubs.glob.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.equal( 'my-packages/*/' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.be.an( 'object' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'cwd', 'C:/Users/ckeditor' ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - const [ firstWorker, secondWorker ] = WorkerMock.instances; + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + await promise; + } ); - await promise; + it( 'should normalize the current working directory to unix-style (`options.cwd`, Windows path)', async () => { + const options = Object.assign( {}, defaultOptions, { + cwd: 'C:\\Users\\ckeditor' } ); - it( 'should normalize the current working directory to unix-style (`options.cwd`, Windows path)', async () => { - const options = Object.assign( {}, defaultOptions, { - cwd: 'C:\\Users\\ckeditor' - } ); - - const promise = executeInParallel( options ); - await delay( 0 ); + const promise = executeInParallel( options ); + await delay( 0 ); - expect( stubs.glob.glob.callCount ).to.equal( 1 ); - expect( stubs.glob.glob.firstCall.args[ 0 ] ).to.equal( 'my-packages/*/' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.be.an( 'object' ); - expect( stubs.glob.glob.firstCall.args[ 1 ] ).to.have.property( 'cwd', 'C:/Users/ckeditor' ); + expect( glob ).toHaveBeenCalledTimes( 1 ); + expect( glob ).toHaveBeenCalledWith( 'my-packages/*/', expect.objectContaining( { + cwd: 'C:/Users/ckeditor', + absolute: true + } ) ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - await promise; - } ); + await promise; + } ); - it( 'should work on normalized paths to packages', async () => { - stubs.glob.glob.resolves( [ - 'C:/Users/workspace/ckeditor/my-packages/package-01', - 'C:/Users/workspace/ckeditor/my-packages/package-02', - 'C:/Users/workspace/ckeditor/my-packages/package-03', - 'C:/Users/workspace/ckeditor/my-packages/package-04' - ] ); + it( 'should work on normalized paths to packages', async () => { + vi.mocked( glob ).mockResolvedValue( [ + 'C:/Users/workspace/ckeditor/my-packages/package-01', + 'C:/Users/workspace/ckeditor/my-packages/package-02', + 'C:/Users/workspace/ckeditor/my-packages/package-03', + 'C:/Users/workspace/ckeditor/my-packages/package-04' + ] ); - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - // By default the helper uses a half of available CPUs. - expect( WorkerMock.instances ).to.lengthOf( 2 ); + // By default the helper uses a half of available CPUs. + expect( stubs.WorkerMock.instances ).toHaveLength( 2 ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - expect( firstWorker.workerData.packages ).to.deep.equal( [ - 'C:/Users/workspace/ckeditor/my-packages/package-01', - 'C:/Users/workspace/ckeditor/my-packages/package-03' - ] ); + expect( firstWorker.workerData.packages ).toEqual( [ + 'C:/Users/workspace/ckeditor/my-packages/package-01', + 'C:/Users/workspace/ckeditor/my-packages/package-03' + ] ); - expect( secondWorker.workerData.packages ).to.deep.equal( [ - 'C:/Users/workspace/ckeditor/my-packages/package-02', - 'C:/Users/workspace/ckeditor/my-packages/package-04' - ] ); + expect( secondWorker.workerData.packages ).toEqual( [ + 'C:/Users/workspace/ckeditor/my-packages/package-02', + 'C:/Users/workspace/ckeditor/my-packages/package-04' + ] ); - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - await promise; - } ); + await promise; + } ); - it( 'should pass task options to all workers', async () => { - const taskOptions = { - property: 'Example of the property.', - some: { - deeply: { - nested: { - property: 'Example the deeply nested property.' - } + it( 'should pass task options to all workers', async () => { + const taskOptions = { + property: 'Example of the property.', + some: { + deeply: { + nested: { + property: 'Example the deeply nested property.' } } - }; + } + }; - const options = Object.assign( {}, defaultOptions, { taskOptions } ); + const options = Object.assign( {}, defaultOptions, { taskOptions } ); - const promise = executeInParallel( options ); - await delay( 0 ); + const promise = executeInParallel( options ); + await delay( 0 ); - // By default the helper uses a half of available CPUs. - expect( WorkerMock.instances ).to.lengthOf( 2 ); + // By default the helper uses a half of available CPUs. + expect( stubs.WorkerMock.instances ).toHaveLength( 2 ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - expect( firstWorker.workerData ).to.be.an( 'object' ); - expect( firstWorker.workerData ).to.have.deep.property( 'taskOptions', taskOptions ); + expect( firstWorker.workerData ).toBeInstanceOf( Object ); + expect( firstWorker.workerData ).toHaveProperty( 'taskOptions', taskOptions ); - expect( secondWorker.workerData ).to.be.an( 'object' ); - expect( secondWorker.workerData ).to.have.property( 'taskOptions', taskOptions ); + expect( secondWorker.workerData ).toBeInstanceOf( Object ); + expect( secondWorker.workerData ).toHaveProperty( 'taskOptions', taskOptions ); - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - await promise; - } ); + await promise; + } ); - it( 'should create the temporary module properly when using Windows-style paths', async () => { - stubs.process.cwd.returns( 'C:\\Users\\ckeditor' ); + it( 'should create the temporary module properly when using Windows-style paths', async () => { + process.cwd.mockReturnValue( 'C:\\Users\\ckeditor' ); - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - expect( stubs.fs.writeFile.callCount ).to.equal( 1 ); - expect( stubs.fs.writeFile.firstCall.args[ 0 ] ).to.equal( 'C:/Users/ckeditor/uuid-4.cjs' ); - expect( stubs.fs.writeFile.firstCall.args[ 1 ] ).to.equal( - '\'use strict\';\nmodule.exports = packagePath => console.log( \'pwd\', packagePath );' - ); + expect( fs.writeFile ).toHaveBeenCalledTimes( 1 ); + expect( fs.writeFile ).toHaveBeenCalledWith( + 'C:/Users/ckeditor/uuid-4.mjs', + 'export default packagePath => console.log( \'pwd\', packagePath );', + 'utf-8' + ); + + // By default the helper uses a half of available CPUs. + expect( stubs.WorkerMock.instances ).toHaveLength( 2 ); - // By default the helper uses a half of available CPUs. - expect( WorkerMock.instances ).to.lengthOf( 2 ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - const [ firstWorker, secondWorker ] = WorkerMock.instances; + expect( firstWorker.workerData ).toBeInstanceOf( Object ); + expect( firstWorker.workerData ).toHaveProperty( 'callbackModule', 'C:/Users/ckeditor/uuid-4.mjs' ); + expect( firstWorker.workerData ).toHaveProperty( 'packages' ); - expect( firstWorker.workerData ).to.be.an( 'object' ); - expect( firstWorker.workerData ).to.have.property( 'callbackModule', 'C:/Users/ckeditor/uuid-4.cjs' ); - expect( firstWorker.workerData ).to.have.property( 'packages' ); + expect( secondWorker.workerData ).toBeInstanceOf( Object ); + expect( secondWorker.workerData ).toHaveProperty( 'callbackModule', 'C:/Users/ckeditor/uuid-4.mjs' ); + expect( secondWorker.workerData ).toHaveProperty( 'packages' ); - expect( secondWorker.workerData ).to.be.an( 'object' ); - expect( secondWorker.workerData ).to.have.property( 'callbackModule', 'C:/Users/ckeditor/uuid-4.cjs' ); - expect( secondWorker.workerData ).to.have.property( 'packages' ); + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + await promise; + } ); - await promise; + it( 'should use the specified number of threads (`concurrency`)', async () => { + const options = Object.assign( {}, defaultOptions, { + concurrency: 4 } ); - it( 'should use the specified number of threads (`concurrency`)', async () => { - const options = Object.assign( {}, defaultOptions, { - concurrency: 4 - } ); + const promise = executeInParallel( options ); + await delay( 0 ); - const promise = executeInParallel( options ); - await delay( 0 ); + expect( stubs.WorkerMock.instances ).toHaveLength( 4 ); - expect( WorkerMock.instances ).to.lengthOf( 4 ); + // Workers did not emit an error. + for ( const worker of stubs.WorkerMock.instances ) { + getExitCallback( worker )( 0 ); + } - // Workers did not emit an error. - for ( const worker of WorkerMock.instances ) { - getExitCallback( worker )( 0 ); - } + await promise; + } ); - await promise; - } ); + it( 'should use number of cores divided by two as default (`concurrency`)', async () => { + vi.mocked( os.cpus ).mockReturnValue( new Array( 7 ) ); - it( 'should resolve the promise if a worker finished (aborted) with a non-zero exit code (first worker)', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; + expect( stubs.WorkerMock.instances ).toHaveLength( 3 ); - getExitCallback( firstWorker )( 1 ); - getExitCallback( secondWorker )( 0 ); + // Workers did not emit an error. + for ( const worker of stubs.WorkerMock.instances ) { + getExitCallback( worker )( 0 ); + } + + await promise; + } ); - await promise; + it( 'should round down to the closest integer (`concurrency`)', async () => { + const options = Object.assign( {}, defaultOptions, { + concurrency: 3.5 } ); - it( 'should resolve the promise if a worker finished (aborted) with a non-zero exit code (second worker)', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + const promise = executeInParallel( options ); + await delay( 0 ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; + expect( stubs.WorkerMock.instances ).toHaveLength( 3 ); - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 1 ); + // Workers did not emit an error. + for ( const worker of stubs.WorkerMock.instances ) { + getExitCallback( worker )( 0 ); + } + + await promise; + } ); - await promise; + it( 'should assign at least one thread even if concurrency is 0 (`concurrency`)', async () => { + const options = Object.assign( {}, defaultOptions, { + concurrency: 0 } ); - it( 'should reject the promise if a worker emitted an error (first worker)', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + const promise = executeInParallel( options ); + await delay( 0 ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; - const error = new Error( 'Example error from a worker.' ); + expect( stubs.WorkerMock.instances ).toHaveLength( 1 ); - getErrorCallback( firstWorker )( error ); - getExitCallback( secondWorker )( 0 ); + // Workers did not emit an error. + for ( const worker of stubs.WorkerMock.instances ) { + getExitCallback( worker )( 0 ); + } - return promise - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - err => { - expect( err ).to.equal( error ); - } - ); - } ); + await promise; + } ); - it( 'should reject the promise if a worker emitted an error (second worker)', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + it( 'should resolve the promise if a worker finished (aborted) with a non-zero exit code (first worker)', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; - const error = new Error( 'Example error from a worker.' ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - getExitCallback( firstWorker )( 0 ); - getErrorCallback( secondWorker )( error ); + getExitCallback( firstWorker )( 1 ); + getExitCallback( secondWorker )( 0 ); - return promise - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - err => { - expect( err ).to.equal( error ); - } - ); - } ); + await promise; + } ); - it( 'should split packages into threads one by one', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); - - const [ firstWorker, secondWorker ] = WorkerMock.instances; - - expect( firstWorker.workerData ).to.be.an( 'object' ); - expect( firstWorker.workerData ).to.have.property( 'packages' ); - expect( firstWorker.workerData.packages ).to.be.an( 'array' ); - expect( firstWorker.workerData.packages ).to.deep.equal( [ - '/home/ckeditor/my-packages/package-01', - '/home/ckeditor/my-packages/package-03' - ] ); - - expect( secondWorker.workerData ).to.be.an( 'object' ); - expect( secondWorker.workerData ).to.have.property( 'packages' ); - expect( secondWorker.workerData.packages ).to.be.an( 'array' ); - expect( secondWorker.workerData.packages ).to.deep.equal( [ - '/home/ckeditor/my-packages/package-02', - '/home/ckeditor/my-packages/package-04' - ] ); - - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); - - await promise; - } ); + it( 'should resolve the promise if a worker finished (aborted) with a non-zero exit code (second worker)', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - it( 'should remove the temporary module after execution', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - const [ firstWorker, secondWorker ] = WorkerMock.instances; + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 1 ); - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + await promise; + } ); - await promise; + it( 'should reject the promise if a worker emitted an error (first worker)', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - expect( stubs.fs.unlink.callCount ).to.equal( 1 ); - expect( stubs.fs.unlink.firstCall.args[ 0 ] ).to.equal( '/home/ckeditor/uuid-4.cjs' ); - } ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; + const error = new Error( 'Example error from a worker.' ); - it( 'should remove the temporary module if the process is aborted', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + getErrorCallback( firstWorker )( error ); + getExitCallback( secondWorker )( 0 ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; + return promise + .then( + () => { + throw new Error( 'Expected to be rejected.' ); + }, + err => { + expect( err ).toEqual( error ); + } + ); + } ); - abortController.abort( 'SIGINT' ); + it( 'should reject the promise if a worker emitted an error (second worker)', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - // Simulate the "Worker#terminate()" behavior. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; + const error = new Error( 'Example error from a worker.' ); - await promise; + getExitCallback( firstWorker )( 0 ); + getErrorCallback( secondWorker )( error ); - expect( stubs.fs.unlink.callCount ).to.equal( 1 ); - expect( stubs.fs.unlink.firstCall.args[ 0 ] ).to.equal( '/home/ckeditor/uuid-4.cjs' ); - } ); + return promise + .then( + () => { + throw new Error( 'Expected to be rejected.' ); + }, + err => { + expect( err ).toEqual( error ); + } + ); + } ); - it( 'should remove the temporary module if the promise rejected', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + it( 'should split packages into threads one by one', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); + + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; + + expect( firstWorker.workerData ).toBeInstanceOf( Object ); + expect( firstWorker.workerData ).toHaveProperty( 'packages' ); + expect( firstWorker.workerData.packages ).toBeInstanceOf( Array ); + expect( firstWorker.workerData.packages ).toEqual( [ + '/home/ckeditor/my-packages/package-01', + '/home/ckeditor/my-packages/package-03' + ] ); + + expect( secondWorker.workerData ).toBeInstanceOf( Object ); + expect( secondWorker.workerData ).toHaveProperty( 'packages' ); + expect( secondWorker.workerData.packages ).toBeInstanceOf( Array ); + expect( secondWorker.workerData.packages ).toEqual( [ + '/home/ckeditor/my-packages/package-02', + '/home/ckeditor/my-packages/package-04' + ] ); + + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); + + await promise; + } ); - const [ firstWorker ] = WorkerMock.instances; - const error = new Error( 'Example error from a worker.' ); + it( 'should remove the temporary module after execution', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - getErrorCallback( firstWorker )( error ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - return promise - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - () => { - expect( stubs.fs.unlink.callCount ).to.equal( 1 ); - expect( stubs.fs.unlink.firstCall.args[ 0 ] ).to.equal( '/home/ckeditor/uuid-4.cjs' ); - } - ); - } ); + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - it( 'should terminate threads if the process is aborted', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + await promise; - const [ firstWorker, secondWorker ] = WorkerMock.instances; + expect( fs.unlink ).toHaveBeenCalledTimes( 1 ); + expect( fs.unlink ).toHaveBeenCalledWith( '/home/ckeditor/uuid-4.mjs' ); + } ); - abortController.abort( 'SIGINT' ); + it( 'should remove the temporary module if the process is aborted', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - // Simulate the "Worker#terminate()" behavior. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - await promise; + abortController.abort( 'SIGINT' ); - expect( firstWorker.terminate.callCount ).to.equal( 1 ); - expect( secondWorker.terminate.callCount ).to.equal( 1 ); - } ); + // Simulate the "Worker#terminate()" behavior. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - it( 'should attach listener to a worker that executes a callback once per worker', async () => { - const signalEvent = sinon.stub( abortController.signal, 'addEventListener' ); - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + await promise; - expect( stubs.abortController.registerAbortController.callCount ).to.equal( 0 ); + expect( fs.unlink ).toHaveBeenCalledTimes( 1 ); + expect( fs.unlink ).toHaveBeenCalledWith( '/home/ckeditor/uuid-4.mjs' ); + } ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; + it( 'should remove the temporary module if the promise rejected', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - abortController.abort( 'SIGINT' ); + const [ firstWorker ] = stubs.WorkerMock.instances; + const error = new Error( 'Example error from a worker.' ); - // Simulate the "Worker#terminate()" behavior. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + getErrorCallback( firstWorker )( error ); + + return promise + .then( + () => { + throw new Error( 'Expected to be rejected.' ); + }, + () => { + expect( fs.unlink ).toHaveBeenCalledTimes( 1 ); + expect( fs.unlink ).toHaveBeenCalledWith( '/home/ckeditor/uuid-4.mjs' ); + } + ); + } ); - await promise; + it( 'should terminate threads if the process is aborted', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - expect( signalEvent.callCount ).to.equal( 2 ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - expect( signalEvent.firstCall.args[ 0 ] ).to.equal( 'abort' ); - expect( signalEvent.firstCall.args[ 1 ] ).to.be.a( 'function' ); - expect( signalEvent.firstCall.args[ 2 ] ).to.be.an( 'object' ); - expect( signalEvent.firstCall.args[ 2 ] ).to.have.property( 'once', true ); + abortController.abort( 'SIGINT' ); - expect( signalEvent.secondCall.args[ 0 ] ).to.equal( 'abort' ); - expect( signalEvent.secondCall.args[ 1 ] ).to.be.a( 'function' ); - expect( signalEvent.secondCall.args[ 2 ] ).to.be.an( 'object' ); - expect( signalEvent.secondCall.args[ 2 ] ).to.have.property( 'once', true ); + // Simulate the "Worker#terminate()" behavior. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - expect( stubs.abortController.deregisterAbortController.callCount ).to.equal( 0 ); - } ); + await promise; - it( 'should register and deregister default abort controller if signal is not provided', async () => { - const abortController = new AbortController(); - const signalEvent = sinon.stub( abortController.signal, 'addEventListener' ); + expect( firstWorker.terminate ).toHaveBeenCalledTimes( 1 ); + expect( secondWorker.terminate ).toHaveBeenCalledTimes( 1 ); + } ); - stubs.abortController.registerAbortController.returns( abortController ); + it( 'should attach listener to a worker that executes a callback once per worker', async () => { + const signalEvent = vi.spyOn( abortController.signal, 'addEventListener' ); + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); + + expect( registerAbortController ).toHaveBeenCalledTimes( 0 ); + + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; + + abortController.abort( 'SIGINT' ); + + // Simulate the "Worker#terminate()" behavior. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); + + await promise; + + expect( signalEvent ).toHaveBeenCalledTimes( 2 ); + expect( signalEvent ).toHaveBeenNthCalledWith( + 1, + 'abort', + expect.any( Function ), + expect.objectContaining( { + once: true + } ) + ); + expect( signalEvent ).toHaveBeenNthCalledWith( + 2, + 'abort', + expect.any( Function ), + expect.objectContaining( { + once: true + } ) + ); + + expect( deregisterAbortController ).toHaveBeenCalledTimes( 0 ); + } ); - const options = Object.assign( {}, defaultOptions ); - delete options.signal; + it( 'should register and deregister default abort controller if signal is not provided', async () => { + const abortController = new AbortController(); + const signalEvent = vi.spyOn( abortController.signal, 'addEventListener' ); - const promise = executeInParallel( options ); - await delay( 0 ); + registerAbortController.mockReturnValue( abortController ); - expect( stubs.abortController.registerAbortController.callCount ).to.equal( 1 ); + const options = Object.assign( {}, defaultOptions ); + delete options.signal; - const [ firstWorker, secondWorker ] = WorkerMock.instances; + const promise = executeInParallel( options ); + await delay( 0 ); - abortController.abort( 'SIGINT' ); + expect( registerAbortController ).toHaveBeenCalledTimes( 1 ); - // Simulate the "Worker#terminate()" behavior. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; - await promise; + abortController.abort( 'SIGINT' ); - expect( signalEvent.callCount ).to.equal( 2 ); + // Simulate the "Worker#terminate()" behavior. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - expect( signalEvent.firstCall.args[ 0 ] ).to.equal( 'abort' ); - expect( signalEvent.firstCall.args[ 1 ] ).to.be.a( 'function' ); - expect( signalEvent.firstCall.args[ 2 ] ).to.be.an( 'object' ); - expect( signalEvent.firstCall.args[ 2 ] ).to.have.property( 'once', true ); + await promise; - expect( signalEvent.secondCall.args[ 0 ] ).to.equal( 'abort' ); - expect( signalEvent.secondCall.args[ 1 ] ).to.be.a( 'function' ); - expect( signalEvent.secondCall.args[ 2 ] ).to.be.an( 'object' ); - expect( signalEvent.secondCall.args[ 2 ] ).to.have.property( 'once', true ); + expect( signalEvent ).toHaveBeenCalledTimes( 2 ); + expect( signalEvent ).toHaveBeenNthCalledWith( + 1, + 'abort', + expect.any( Function ), + expect.objectContaining( { + once: true + } ) + ); + expect( signalEvent ).toHaveBeenNthCalledWith( + 2, + 'abort', + expect.any( Function ), + expect.objectContaining( { + once: true + } ) + ); - expect( stubs.abortController.deregisterAbortController.callCount ).to.equal( 1 ); - } ); + expect( deregisterAbortController ).toHaveBeenCalledTimes( 1 ); + } ); - it( 'should update the progress when a package finished executing the callback', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); - - const [ firstWorker, secondWorker ] = WorkerMock.instances; - - const firstWorkerPackageDone = getMessageCallback( firstWorker ); - const secondWorkerPackageDone = getMessageCallback( secondWorker ); - - expect( outputHistory ).to.lengthOf( 0 ); - firstWorkerPackageDone( 'done:package' ); - expect( outputHistory ).to.include( 'Status: 1/4.' ); - expect( outputHistory ).to.lengthOf( 1 ); - secondWorkerPackageDone( 'done:package' ); - expect( outputHistory ).to.include( 'Status: 2/4.' ); - expect( outputHistory ).to.lengthOf( 2 ); - secondWorkerPackageDone( 'done:package' ); - expect( outputHistory ).to.lengthOf( 3 ); - expect( outputHistory ).to.include( 'Status: 3/4.' ); - firstWorkerPackageDone( 'done:package' ); - expect( outputHistory ).to.lengthOf( 4 ); - expect( outputHistory ).to.include( 'Status: 4/4.' ); - - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); - - await promise; - } ); + it( 'should update the progress when a package finished executing the callback', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); + + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; + + const firstWorkerPackageDone = getMessageCallback( firstWorker ); + const secondWorkerPackageDone = getMessageCallback( secondWorker ); + + expect( outputHistory ).toHaveLength( 0 ); + firstWorkerPackageDone( 'done:package' ); + expect( outputHistory ).toContain( 'Status: 1/4.' ); + expect( outputHistory ).toHaveLength( 1 ); + secondWorkerPackageDone( 'done:package' ); + expect( outputHistory ).toContain( 'Status: 2/4.' ); + expect( outputHistory ).toHaveLength( 2 ); + secondWorkerPackageDone( 'done:package' ); + expect( outputHistory ).toHaveLength( 3 ); + expect( outputHistory ).toContain( 'Status: 3/4.' ); + firstWorkerPackageDone( 'done:package' ); + expect( outputHistory ).toHaveLength( 4 ); + expect( outputHistory ).toContain( 'Status: 4/4.' ); + + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); + + await promise; + } ); - it( 'should ignore messages from threads unrelated to the progress', async () => { - const promise = executeInParallel( defaultOptions ); - await delay( 0 ); + it( 'should ignore messages from threads unrelated to the progress', async () => { + const promise = executeInParallel( defaultOptions ); + await delay( 0 ); - const [ firstWorker, secondWorker ] = WorkerMock.instances; - const firstWorkerPackageDone = getMessageCallback( firstWorker ); + const [ firstWorker, secondWorker ] = stubs.WorkerMock.instances; + const firstWorkerPackageDone = getMessageCallback( firstWorker ); - expect( outputHistory ).to.lengthOf( 0 ); - firstWorkerPackageDone( 'foo' ); - expect( outputHistory ).to.lengthOf( 0 ); + expect( outputHistory ).toHaveLength( 0 ); + firstWorkerPackageDone( 'foo' ); + expect( outputHistory ).toHaveLength( 0 ); - // Workers did not emit an error. - getExitCallback( firstWorker )( 0 ); - getExitCallback( secondWorker )( 0 ); + // Workers did not emit an error. + getExitCallback( firstWorker )( 0 ); + getExitCallback( secondWorker )( 0 ); - await promise; - } ); + await promise; } ); } ); @@ -637,8 +692,8 @@ function delay( time ) { } function getExitCallback( fakeWorker ) { - for ( const call of fakeWorker.on.getCalls() ) { - const [ eventName, callback ] = call.args; + for ( const call of fakeWorker.on.mock.calls ) { + const [ eventName, callback ] = call; if ( eventName === 'exit' ) { return callback; @@ -647,8 +702,8 @@ function getExitCallback( fakeWorker ) { } function getMessageCallback( fakeWorker ) { - for ( const call of fakeWorker.on.getCalls() ) { - const [ eventName, callback ] = call.args; + for ( const call of fakeWorker.on.mock.calls ) { + const [ eventName, callback ] = call; if ( eventName === 'message' ) { return callback; @@ -657,8 +712,8 @@ function getMessageCallback( fakeWorker ) { } function getErrorCallback( fakeWorker ) { - for ( const call of fakeWorker.on.getCalls() ) { - const [ eventName, callback ] = call.args; + for ( const call of fakeWorker.on.mock.calls ) { + const [ eventName, callback ] = call; if ( eventName === 'error' ) { return callback; diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/generatechangelog.js b/packages/ckeditor5-dev-release-tools/tests/utils/generatechangelog.js index 722b40ccd..29b81e94b 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/generatechangelog.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/generatechangelog.js @@ -3,261 +3,551 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect } from 'vitest'; +import compareFunc from 'compare-func'; +import getWriterOptions from '../../lib/utils/getwriteroptions.js'; +import generateChangelog from '../../lib/utils/generatechangelog.js'; -const expect = require( 'chai' ).expect; -const compareFunc = require( 'compare-func' ); -const getWriterOptions = require( '../../lib/utils/getwriteroptions' ); -const generateChangelog = require( '../../lib/utils/generatechangelog' ); - -describe( 'dev-release-tools/utils', () => { +describe( 'generateChangelog()', () => { const url = 'https://github.com/ckeditor/ckeditor5-package'; /** * Type of commits must be equal to values returned by `transformcommitutils.getCommitType()` function. * Since we're creating all commits manually, we need to "transform" those to proper structures. */ - describe( 'generateChangelog()', () => { - describe( 'initial changelog (without "previousTag")', () => { - it( 'generates "Features" correctly', () => { - const commits = [ - { - type: 'Features', - header: 'Feature: The first an amazing feature.', - subject: 'The first an amazing feature.', - hash: 'x'.repeat( 40 ), - notes: [] - }, - { - type: 'Features', - header: 'Feature: The second an amazing feature.', - subject: 'The second an amazing feature.', - hash: 'z'.repeat( 40 ), - notes: [] - } - ]; + describe( 'initial changelog (without "previousTag")', () => { + it( 'generates "Features" correctly', () => { + const commits = [ + { + type: 'Features', + header: 'Feature: The first an amazing feature.', + subject: 'The first an amazing feature.', + hash: 'x'.repeat( 40 ), + notes: [] + }, + { + type: 'Features', + header: 'Feature: The second an amazing feature.', + subject: 'The second an amazing feature.', + hash: 'z'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + commit: 'commit' + }; + + const options = getWriterOptions( transformCommitCallback( 7 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + '### Features' + ); + expect( changesAsArray[ 2 ] ).toEqual( + '* The first an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))' + ); + expect( changesAsArray[ 3 ] ).toEqual( + '* The second an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' + ); + } ); + } ); - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'commit' - }; + it( 'generates "Bug fixes" correctly', () => { + const commits = [ + { + type: 'Bug fixes', + header: 'Fix: The first an amazing bug fix.', + subject: 'The first an amazing bug fix.', + hash: 'x'.repeat( 40 ), + notes: [] + }, + { + type: 'Bug fixes', + header: 'Fix: The second an amazing bug fix.', + subject: 'The second an amazing bug fix.', + hash: 'z'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + commit: 'commit' + }; + + const options = getWriterOptions( transformCommitCallback( 7 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + '### Bug fixes' + ); + expect( changesAsArray[ 2 ] ).toEqual( + '* The first an amazing bug fix. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))' + ); + expect( changesAsArray[ 3 ] ).toEqual( + '* The second an amazing bug fix. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' + ); + } ); + } ); - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) + it( 'generates "Other changes" correctly', () => { + const commits = [ + { + type: 'Other changes', + header: 'Other: The first an amazing commit.', + subject: 'The first an amazing commit.', + hash: 'x'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: The second an amazing commit.', + subject: 'The second an amazing commit.', + hash: 'z'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + commit: 'commit' + }; + + const options = getWriterOptions( transformCommitCallback( 7 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + '### Other changes' + ); + expect( changesAsArray[ 2 ] ).toEqual( + '* The first an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))' + ); + expect( changesAsArray[ 3 ] ).toEqual( + '* The second an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' + ); } ); + } ); - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); + it( 'generates all groups correctly', () => { + const commits = [ + { + type: 'Features', + header: 'Feature: An amazing feature.', + subject: 'An amazing feature.', + hash: 'x'.repeat( 40 ), + notes: [] + }, + { + type: 'Bug fixes', + header: 'Fix: An amazing bug fix.', + subject: 'An amazing bug fix.', + hash: 'z'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: An amazing commit.', + subject: 'An amazing commit.', + hash: 'y'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + commit: 'commit' + }; + + const options = getWriterOptions( transformCommitCallback( 7 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + '### Features' + ); + expect( changesAsArray[ 2 ] ).toEqual( + '* An amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))' + ); + expect( changesAsArray[ 3 ] ).toEqual( + '### Bug fixes' + ); + expect( changesAsArray[ 4 ] ).toEqual( + '* An amazing bug fix. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' + ); + expect( changesAsArray[ 5 ] ).toEqual( + '### Other changes' + ); + expect( changesAsArray[ 6 ] ).toEqual( + '* An amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/yyyyyyy))' + ); + } ); + } ); - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); + it( 'removes URLs to commits (context.skipCommitsLink=true)', () => { + const commits = [ + { + type: 'Features', + header: 'Feature: The first an amazing feature.', + subject: 'The first an amazing feature.', + hash: 'x'.repeat( 40 ), + notes: [] + }, + { + type: 'Features', + header: 'Feature: The second an amazing feature.', + subject: 'The second an amazing feature.', + hash: 'z'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + skipCommitsLink: true + }; + + const options = getWriterOptions( transformCommitCallback( 7 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + '### Features' + ); + expect( changesAsArray[ 2 ] ).toEqual( + '* The first an amazing feature.' + ); + expect( changesAsArray[ 3 ] ).toEqual( + '* The second an amazing feature.' + ); + } ); + } ); - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - '### Features' - ); - expect( changesAsArray[ 2 ] ).to.equal( - '* The first an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))' - ); - expect( changesAsArray[ 3 ] ).to.equal( - '* The second an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' - ); - } ); - } ); + it( 'removes compare link from the header (context.skipCompareLink=true)', () => { + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + commit: 'commit', + skipCompareLink: true + }; - it( 'generates "Bug fixes" correctly', () => { - const commits = [ - { - type: 'Bug fixes', - header: 'Fix: The first an amazing bug fix.', - subject: 'The first an amazing bug fix.', - hash: 'x'.repeat( 40 ), - notes: [] - }, - { - type: 'Bug fixes', - header: 'Fix: The second an amazing bug fix.', - subject: 'The second an amazing bug fix.', - hash: 'z'.repeat( 40 ), - notes: [] - } - ]; + const options = getWriterOptions( transformCommitCallback( 7 ) ); - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'commit' - }; + return generateChangelog( [], context, options ) + .then( changes => { + changes = replaceDates( changes ); - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## 1.0.0 (0000-00-00)' + ); } ); + } ); - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); + it( 'generates additional commit message below the subject', () => { + const commits = [ + { + type: 'Other changes', + header: 'Other: The first an amazing commit.', + subject: 'The first an amazing commit.', + body: [ + ' First line: Lorem Ipsum (1).', + ' Second line: Lorem Ipsum (2).' + ].join( '\n' ), + hash: 'x'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: The second an amazing commit.', + subject: 'The second an amazing commit.', + body: [ + ' First line: Lorem Ipsum (1).', + ' Second line: Lorem Ipsum (2).', + ' Third line: Lorem Ipsum (3).' + ].join( '\n' ), + hash: 'z'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: The third an amazing commit.', + subject: 'The third an amazing commit.', + hash: 'y'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + commit: 'commit' + }; + + const options = getWriterOptions( transformCommitCallback( 7 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changelog = [ + '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)', + '', + '### Other changes', + '', + '* The first an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))', + '', + ' First line: Lorem Ipsum (1).', + ' Second line: Lorem Ipsum (2).', + '* The second an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))', + '', + ' First line: Lorem Ipsum (1).', + ' Second line: Lorem Ipsum (2).', + ' Third line: Lorem Ipsum (3).', + '* The third an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/yyyyyyy))' + ].join( '\n' ); + + expect( changes.trim() ).toEqual( changelog ); + } ); + } ); - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); + it( 'groups "Updated translations." commits as the single entry (merged links)', () => { + const commits = [ + { + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'a'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'b'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'c'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'd'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + commit: 'c' + }; + + const options = getWriterOptions( transformCommitCallback( 2 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + '### Other changes' + ); + /* eslint-disable max-len */ + expect( changesAsArray[ 2 ] ).toEqual( + '* Updated translations. ([commit](https://github.com/ckeditor/ckeditor5-package/c/aa), [commit](https://github.com/ckeditor/ckeditor5-package/c/bb), [commit](https://github.com/ckeditor/ckeditor5-package/c/cc), [commit](https://github.com/ckeditor/ckeditor5-package/c/dd))' + ); + /* eslint-enable max-len */ + } ); + } ); - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - '### Bug fixes' - ); - expect( changesAsArray[ 2 ] ).to.equal( - '* The first an amazing bug fix. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))' - ); - expect( changesAsArray[ 3 ] ).to.equal( - '* The second an amazing bug fix. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' - ); - } ); - } ); + it( 'groups "Updated translations." commits as the single entry (removed links, context.skipCommitsLink=true)', () => { + const commits = [ + { + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'a'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'b'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'c'.repeat( 40 ), + notes: [] + }, + { + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'd'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + skipCommitsLink: true + }; + + const options = getWriterOptions( transformCommitCallback( 2 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + '### Other changes' + ); + expect( changesAsArray[ 2 ] ).toEqual( + '* Updated translations.' + ); + } ); + } ); - it( 'generates "Other changes" correctly', () => { + // See: https://github.com/ckeditor/ckeditor5/issues/10445. + it( + 'groups "Updated translations." commits as the single entry (merged links) even if a commit specified "skipLinks=true ' + + '(a private commit is in the middle of the collection)', + () => { const commits = [ { type: 'Other changes', - header: 'Other: The first an amazing commit.', - subject: 'The first an amazing commit.', - hash: 'x'.repeat( 40 ), + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'a'.repeat( 40 ), notes: [] }, { type: 'Other changes', - header: 'Other: The second an amazing commit.', - subject: 'The second an amazing commit.', - hash: 'z'.repeat( 40 ), - notes: [] - } - ]; - - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'commit' - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) - } ); - - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - '### Other changes' - ); - expect( changesAsArray[ 2 ] ).to.equal( - '* The first an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))' - ); - expect( changesAsArray[ 3 ] ).to.equal( - '* The second an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' - ); - } ); - } ); - - it( 'generates all groups correctly', () => { - const commits = [ - { - type: 'Features', - header: 'Feature: An amazing feature.', - subject: 'An amazing feature.', + header: 'Other: Updated translations.', + subject: 'Updated translations.', hash: 'x'.repeat( 40 ), - notes: [] + notes: [], + skipCommitsLink: true }, { - type: 'Bug fixes', - header: 'Fix: An amazing bug fix.', - subject: 'An amazing bug fix.', - hash: 'z'.repeat( 40 ), + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'b'.repeat( 40 ), notes: [] }, { type: 'Other changes', - header: 'Other: An amazing commit.', - subject: 'An amazing commit.', - hash: 'y'.repeat( 40 ), + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'c'.repeat( 40 ), notes: [] - } - ]; - - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'commit' - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) - } ); - - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - '### Features' - ); - expect( changesAsArray[ 2 ] ).to.equal( - '* An amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))' - ); - expect( changesAsArray[ 3 ] ).to.equal( - '### Bug fixes' - ); - expect( changesAsArray[ 4 ] ).to.equal( - '* An amazing bug fix. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' - ); - expect( changesAsArray[ 5 ] ).to.equal( - '### Other changes' - ); - expect( changesAsArray[ 6 ] ).to.equal( - '* An amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/yyyyyyy))' - ); - } ); - } ); - - it( 'removes URLs to commits (context.skipCommitsLink=true)', () => { - const commits = [ + }, { - type: 'Features', - header: 'Feature: The first an amazing feature.', - subject: 'The first an amazing feature.', - hash: 'x'.repeat( 40 ), + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'd'.repeat( 40 ), notes: [] }, { - type: 'Features', - header: 'Feature: The second an amazing feature.', - subject: 'The second an amazing feature.', + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', hash: 'z'.repeat( 40 ), - notes: [] + notes: [], + skipCommitsLink: true } ]; @@ -265,12 +555,10 @@ describe( 'dev-release-tools/utils', () => { version: '1.0.0', repoUrl: url, currentTag: 'v1.0.0', - skipCommitsLink: true + commit: 'c' }; - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) - } ); + const options = getWriterOptions( transformCommitCallback( 2 ) ); return generateChangelog( commits, context, options ) .then( changes => { @@ -280,120 +568,34 @@ describe( 'dev-release-tools/utils', () => { .map( line => line.trim() ) .filter( line => line.length ); - expect( changesAsArray[ 0 ] ).to.equal( + expect( changesAsArray[ 0 ] ).toEqual( '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' ); - expect( changesAsArray[ 1 ] ).to.equal( - '### Features' - ); - expect( changesAsArray[ 2 ] ).to.equal( - '* The first an amazing feature.' - ); - expect( changesAsArray[ 3 ] ).to.equal( - '* The second an amazing feature.' + expect( changesAsArray[ 1 ] ).toEqual( + '### Other changes' ); - } ); - } ); - - it( 'removes compare link from the header (context.skipCompareLink=true)', () => { - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'commit', - skipCompareLink: true - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) - } ); - - return generateChangelog( [], context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## 1.0.0 (0000-00-00)' + /* eslint-disable max-len */ + expect( changesAsArray[ 2 ] ).toEqual( + '* Updated translations. ([commit](https://github.com/ckeditor/ckeditor5-package/c/aa), [commit](https://github.com/ckeditor/ckeditor5-package/c/bb), [commit](https://github.com/ckeditor/ckeditor5-package/c/cc), [commit](https://github.com/ckeditor/ckeditor5-package/c/dd))' ); + /* eslint-enable max-len */ } ); } ); - it( 'generates additional commit message below the subject', () => { + // See: https://github.com/ckeditor/ckeditor5/issues/10445. + it( + 'groups "Updated translations." commits as the single entry (merged links) even if a commit specified "skipLinks=true ' + + '(a private commit is at the beginning of the collection)', + () => { const commits = [ { type: 'Other changes', - header: 'Other: The first an amazing commit.', - subject: 'The first an amazing commit.', - body: [ - ' First line: Lorem Ipsum (1).', - ' Second line: Lorem Ipsum (2).' - ].join( '\n' ), + header: 'Other: Updated translations.', + subject: 'Updated translations.', hash: 'x'.repeat( 40 ), - notes: [] - }, - { - type: 'Other changes', - header: 'Other: The second an amazing commit.', - subject: 'The second an amazing commit.', - body: [ - ' First line: Lorem Ipsum (1).', - ' Second line: Lorem Ipsum (2).', - ' Third line: Lorem Ipsum (3).' - ].join( '\n' ), - hash: 'z'.repeat( 40 ), - notes: [] + notes: [], + skipCommitsLink: true }, - { - type: 'Other changes', - header: 'Other: The third an amazing commit.', - subject: 'The third an amazing commit.', - hash: 'y'.repeat( 40 ), - notes: [] - } - ]; - - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'commit' - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) - } ); - - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changelog = [ - '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)', - '', - '### Other changes', - '', - '* The first an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/xxxxxxx))', - '', - ' First line: Lorem Ipsum (1).', - ' Second line: Lorem Ipsum (2).', - '* The second an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))', - '', - ' First line: Lorem Ipsum (1).', - ' Second line: Lorem Ipsum (2).', - ' Third line: Lorem Ipsum (3).', - '* The third an amazing commit. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/yyyyyyy))' - ].join( '\n' ); - - expect( changes.trim() ).to.equal( changelog ); - } ); - } ); - - it( 'groups "Updated translations." commits as the single entry (merged links)', () => { - const commits = [ { type: 'Other changes', header: 'Other: Updated translations.', @@ -421,6 +623,14 @@ describe( 'dev-release-tools/utils', () => { subject: 'Updated translations.', hash: 'd'.repeat( 40 ), notes: [] + }, + { + type: 'Other changes', + header: 'Other: Updated translations.', + subject: 'Updated translations.', + hash: 'z'.repeat( 40 ), + notes: [], + skipCommitsLink: true } ]; @@ -431,9 +641,7 @@ describe( 'dev-release-tools/utils', () => { commit: 'c' }; - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 2 ) - } ); + const options = getWriterOptions( transformCommitCallback( 2 ) ); return generateChangelog( commits, context, options ) .then( changes => { @@ -443,49 +651,57 @@ describe( 'dev-release-tools/utils', () => { .map( line => line.trim() ) .filter( line => line.length ); - expect( changesAsArray[ 0 ] ).to.equal( + expect( changesAsArray[ 0 ] ).toEqual( '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' ); - expect( changesAsArray[ 1 ] ).to.equal( + expect( changesAsArray[ 1 ] ).toEqual( '### Other changes' ); /* eslint-disable max-len */ - expect( changesAsArray[ 2 ] ).to.equal( + expect( changesAsArray[ 2 ] ).toEqual( '* Updated translations. ([commit](https://github.com/ckeditor/ckeditor5-package/c/aa), [commit](https://github.com/ckeditor/ckeditor5-package/c/bb), [commit](https://github.com/ckeditor/ckeditor5-package/c/cc), [commit](https://github.com/ckeditor/ckeditor5-package/c/dd))' ); /* eslint-enable max-len */ } ); } ); - it( 'groups "Updated translations." commits as the single entry (removed links, context.skipCommitsLink=true)', () => { + // See: https://github.com/ckeditor/ckeditor5/issues/10445. + it( + 'groups "Updated translations." commits as the single entry (merged links) even if a commit specified "skipLinks=true ' + + '(all commits come from the private repository)', + () => { const commits = [ { type: 'Other changes', header: 'Other: Updated translations.', subject: 'Updated translations.', hash: 'a'.repeat( 40 ), - notes: [] + notes: [], + skipCommitsLink: true }, { type: 'Other changes', header: 'Other: Updated translations.', subject: 'Updated translations.', hash: 'b'.repeat( 40 ), - notes: [] + notes: [], + skipCommitsLink: true }, { type: 'Other changes', header: 'Other: Updated translations.', subject: 'Updated translations.', hash: 'c'.repeat( 40 ), - notes: [] + notes: [], + skipCommitsLink: true }, { type: 'Other changes', header: 'Other: Updated translations.', subject: 'Updated translations.', hash: 'd'.repeat( 40 ), - notes: [] + notes: [], + skipCommitsLink: true } ]; @@ -493,12 +709,10 @@ describe( 'dev-release-tools/utils', () => { version: '1.0.0', repoUrl: url, currentTag: 'v1.0.0', - skipCommitsLink: true + commit: 'c' }; - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 2 ) - } ); + const options = getWriterOptions( transformCommitCallback( 2 ) ); return generateChangelog( commits, context, options ) .then( changes => { @@ -508,571 +722,330 @@ describe( 'dev-release-tools/utils', () => { .map( line => line.trim() ) .filter( line => line.length ); - expect( changesAsArray[ 0 ] ).to.equal( + expect( changesAsArray[ 0 ] ).toEqual( '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' ); - expect( changesAsArray[ 1 ] ).to.equal( + expect( changesAsArray[ 1 ] ).toEqual( '### Other changes' ); - expect( changesAsArray[ 2 ] ).to.equal( + /* eslint-disable max-len */ + expect( changesAsArray[ 2 ] ).toEqual( '* Updated translations.' ); + /* eslint-enable max-len */ } ); } ); - // See: https://github.com/ckeditor/ckeditor5/issues/10445. - it( - 'groups "Updated translations." commits as the single entry (merged links) even if a commit specified "skipLinks=true ' + - '(a private commit is in the middle of the collection)', - () => { - const commits = [ - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'a'.repeat( 40 ), - notes: [] - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'x'.repeat( 40 ), - notes: [], - skipCommitsLink: true - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'b'.repeat( 40 ), - notes: [] - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'c'.repeat( 40 ), - notes: [] - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'd'.repeat( 40 ), - notes: [] - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'z'.repeat( 40 ), - notes: [], - skipCommitsLink: true - } - ]; - - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'c' - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 2 ) - } ); - - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - '### Other changes' - ); - /* eslint-disable max-len */ - expect( changesAsArray[ 2 ] ).to.equal( - '* Updated translations. ([commit](https://github.com/ckeditor/ckeditor5-package/c/aa), [commit](https://github.com/ckeditor/ckeditor5-package/c/bb), [commit](https://github.com/ckeditor/ckeditor5-package/c/cc), [commit](https://github.com/ckeditor/ckeditor5-package/c/dd))' - ); - /* eslint-enable max-len */ - } ); + it( 'allows removing a URL to commit per commit', () => { + const commits = [ + { + type: 'Features', + header: 'Feature: (a) The first an amazing feature.', + subject: '(a) The first an amazing feature.', + hash: 'x'.repeat( 40 ), + notes: [], + skipCommitsLink: true + }, + { + type: 'Features', + header: 'Feature: (b) The second an amazing feature.', + subject: '(b) The second an amazing feature.', + hash: 'z'.repeat( 40 ), + notes: [] + }, + { + type: 'Features', + header: 'Feature: (c) The last one an amazing feature.', + subject: '(c) The last one an amazing feature.', + hash: 'y'.repeat( 40 ), + notes: [], + skipCommitsLink: true + } + ]; + + const context = { + version: '1.0.0', + repoUrl: url, + currentTag: 'v1.0.0', + commit: 'commit' + }; + + const options = getWriterOptions( transformCommitCallback( 7 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + '### Features' + ); + expect( changesAsArray[ 2 ] ).toEqual( + '* (a) The first an amazing feature.' + ); + expect( changesAsArray[ 3 ] ).toEqual( + '* (b) The second an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' + ); + expect( changesAsArray[ 4 ] ).toEqual( + '* (c) The last one an amazing feature.' + ); } ); + } ); + } ); - // See: https://github.com/ckeditor/ckeditor5/issues/10445. - it( - 'groups "Updated translations." commits as the single entry (merged links) even if a commit specified "skipLinks=true ' + - '(a private commit is at the beginning of the collection)', - () => { - const commits = [ - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'x'.repeat( 40 ), - notes: [], - skipCommitsLink: true - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'a'.repeat( 40 ), - notes: [] - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'b'.repeat( 40 ), - notes: [] - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'c'.repeat( 40 ), - notes: [] - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'd'.repeat( 40 ), - notes: [] - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'z'.repeat( 40 ), - notes: [], - skipCommitsLink: true - } - ]; - - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'c' - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 2 ) - } ); + describe( 'non-initial changelog (with "previousTag")', () => { + it( 'allows generating "internal release" (set by option, ignored all commits)', () => { + const commits = [ + { + type: 'Other changes', + header: 'Other: The first an amazing commit.', + subject: 'The first an amazing commit.', + hash: 'x'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.1.0', + repoUrl: url, + currentTag: 'v1.1.0', + previousTag: 'v1.0.0', + commit: 'commit', + isInternalRelease: true + }; + + const options = getWriterOptions( transformCommitCallback( 7 ) ); + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.1.0](https://github.com/ckeditor/ckeditor5-package/compare/v1.0.0...v1.1.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + 'Internal changes only (updated dependencies, documentation, etc.).' + ); + } ); + } ); - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - '### Other changes' - ); - /* eslint-disable max-len */ - expect( changesAsArray[ 2 ] ).to.equal( - '* Updated translations. ([commit](https://github.com/ckeditor/ckeditor5-package/c/aa), [commit](https://github.com/ckeditor/ckeditor5-package/c/bb), [commit](https://github.com/ckeditor/ckeditor5-package/c/cc), [commit](https://github.com/ckeditor/ckeditor5-package/c/dd))' - ); - /* eslint-enable max-len */ - } ); + it( 'allows generating "internal release" (passed an empty array of commits)', () => { + const context = { + version: '1.1.0', + repoUrl: url, + currentTag: 'v1.1.0', + previousTag: 'v1.0.0', + commit: 'commit' + }; + + const options = getWriterOptions( transformCommitCallback( 7 ) ); + + return generateChangelog( [], context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.1.0](https://github.com/ckeditor/ckeditor5-package/compare/v1.0.0...v1.1.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + 'Internal changes only (updated dependencies, documentation, etc.).' + ); } ); + } ); - // See: https://github.com/ckeditor/ckeditor5/issues/10445. - it( - 'groups "Updated translations." commits as the single entry (merged links) even if a commit specified "skipLinks=true ' + - '(all commits come from the private repository)', - () => { - const commits = [ + it( 'generates complex changelog', () => { + const commits = [ + { + type: 'Features', + header: 'Feature (engine): The first an amazing feature.', + subject: 'The first an amazing feature.', + scope: [ 'engine' ], + hash: 'x'.repeat( 40 ), + notes: [ { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'a'.repeat( 40 ), - notes: [], - skipCommitsLink: true - }, - { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'b'.repeat( 40 ), - notes: [], - skipCommitsLink: true - }, + title: 'MINOR BREAKING CHANGES', + text: 'Nothing but I would like to use the note - engine.', + scope: [ 'engine' ] + } + ] + }, + { + type: 'Features', + header: 'Feature: The second an amazing feature.', + subject: 'The second an amazing feature.', + hash: 'z'.repeat( 40 ), + notes: [] + }, + { + type: 'Bug fixes', + header: 'Fix (ui): The first amazing bug fix.', + subject: 'The first amazing bug fix.', + scope: [ 'ui' ], + hash: 'y'.repeat( 40 ), + notes: [ { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'c'.repeat( 40 ), - notes: [], - skipCommitsLink: true - }, + title: 'MINOR BREAKING CHANGES', + text: 'Nothing but I would like to use the note - ui.', + scope: [ 'ui' ] + } + ] + }, + { + type: 'Other changes', + header: 'Other: Use the newest version of Node.js on CI.', + subject: 'Use the newest version of Node.js on CI.', + hash: 'a'.repeat( 40 ), + notes: [ { - type: 'Other changes', - header: 'Other: Updated translations.', - subject: 'Updated translations.', - hash: 'd'.repeat( 40 ), - notes: [], - skipCommitsLink: true + title: 'MAJOR BREAKING CHANGES', + text: 'This change should be scoped too but the script should work if the scope is being missed.', + scope: [] } - ]; - - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'c' - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 2 ) - } ); - - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - '### Other changes' - ); - /* eslint-disable max-len */ - expect( changesAsArray[ 2 ] ).to.equal( - '* Updated translations.' - ); - /* eslint-enable max-len */ - } ); - } ); - - it( 'allows removing a URL to commit per commit', () => { - const commits = [ - { - type: 'Features', - header: 'Feature: (a) The first an amazing feature.', - subject: '(a) The first an amazing feature.', - hash: 'x'.repeat( 40 ), - notes: [], - skipCommitsLink: true - }, - { - type: 'Features', - header: 'Feature: (b) The second an amazing feature.', - subject: '(b) The second an amazing feature.', - hash: 'z'.repeat( 40 ), - notes: [] - }, - { - type: 'Features', - header: 'Feature: (c) The last one an amazing feature.', - subject: '(c) The last one an amazing feature.', - hash: 'y'.repeat( 40 ), - notes: [], - skipCommitsLink: true - } - ]; - - const context = { - version: '1.0.0', - repoUrl: url, - currentTag: 'v1.0.0', - commit: 'commit' - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) - } ); - - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.0.0](https://github.com/ckeditor/ckeditor5-package/tree/v1.0.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - '### Features' - ); - expect( changesAsArray[ 2 ] ).to.equal( - '* (a) The first an amazing feature.' - ); - expect( changesAsArray[ 3 ] ).to.equal( - '* (b) The second an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/commit/zzzzzzz))' - ); - expect( changesAsArray[ 4 ] ).to.equal( - '* (c) The last one an amazing feature.' - ); - } ); - } ); - } ); - - describe( 'non-initial changelog (with "previousTag")', () => { - it( 'allows generating "internal release" (set by option, ignored all commits)', () => { - const commits = [ - { - type: 'Other changes', - header: 'Other: The first an amazing commit.', - subject: 'The first an amazing commit.', - hash: 'x'.repeat( 40 ), - notes: [] - } - ]; - - const context = { - version: '1.1.0', - repoUrl: url, - currentTag: 'v1.1.0', - previousTag: 'v1.0.0', - commit: 'commit', - isInternalRelease: true - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) - } ); - - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.1.0](https://github.com/ckeditor/ckeditor5-package/compare/v1.0.0...v1.1.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - 'Internal changes only (updated dependencies, documentation, etc.).' - ); - } ); - } ); - - it( 'allows generating "internal release" (passed an empty array of commits)', () => { - const context = { - version: '1.1.0', - repoUrl: url, - currentTag: 'v1.1.0', - previousTag: 'v1.0.0', - commit: 'commit' - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) - } ); - - return generateChangelog( [], context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.1.0](https://github.com/ckeditor/ckeditor5-package/compare/v1.0.0...v1.1.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - 'Internal changes only (updated dependencies, documentation, etc.).' - ); - } ); + ] + }, + { + type: 'Features', + header: 'Feature (autoformat): It just works.', + subject: 'It just works.', + scope: [ + 'autoformat', + // The tool supports multi-scoped changes but only the first one will be printed in the changelog. + 'engine' + ], + hash: 'b'.repeat( 40 ), + notes: [] + } + ]; + + const context = { + version: '1.1.0', + repoUrl: url, + currentTag: 'v1.1.0', + previousTag: 'v1.0.0', + commit: 'c' + }; + + const options = getWriterOptions( transformCommitCallback( 2 ) ); + + const sortFunction = compareFunc( item => { + if ( Array.isArray( item.scope ) ) { + return item.scope[ 0 ]; + } + + // A hack that allows moving all non-scoped commits or breaking changes notes at the end of the list. + return 'z'.repeat( 15 ); } ); - it( 'generates complex changelog', () => { - const commits = [ - { - type: 'Features', - header: 'Feature (engine): The first an amazing feature.', - subject: 'The first an amazing feature.', - scope: [ 'engine' ], - hash: 'x'.repeat( 40 ), - notes: [ - { - title: 'MINOR BREAKING CHANGES', - text: 'Nothing but I would like to use the note - engine.', - scope: [ 'engine' ] - } - ] - }, - { - type: 'Features', - header: 'Feature: The second an amazing feature.', - subject: 'The second an amazing feature.', - hash: 'z'.repeat( 40 ), - notes: [] - }, - { - type: 'Bug fixes', - header: 'Fix (ui): The first amazing bug fix.', - subject: 'The first amazing bug fix.', - scope: [ 'ui' ], - hash: 'y'.repeat( 40 ), - notes: [ - { - title: 'MINOR BREAKING CHANGES', - text: 'Nothing but I would like to use the note - ui.', - scope: [ 'ui' ] - } - ] - }, - { - type: 'Other changes', - header: 'Other: Use the newest version of Node.js on CI.', - subject: 'Use the newest version of Node.js on CI.', - hash: 'a'.repeat( 40 ), - notes: [ - { - title: 'MAJOR BREAKING CHANGES', - text: 'This change should be scoped too but the script should work if the scope is being missed.', - scope: [] - } - ] - }, - { - type: 'Features', - header: 'Feature (autoformat): It just works.', - subject: 'It just works.', - scope: [ - 'autoformat', - // The tool supports multi-scoped changes but only the first one will be printed in the changelog. - 'engine' - ], - hash: 'b'.repeat( 40 ), - notes: [] - } - ]; - - const context = { - version: '1.1.0', - repoUrl: url, - currentTag: 'v1.1.0', - previousTag: 'v1.0.0', - commit: 'c' - }; - - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 2 ) + options.commitsSort = sortFunction; + options.notesSort = sortFunction; + + return generateChangelog( commits, context, options ) + .then( changes => { + changes = replaceDates( changes ); + + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); + + expect( changesAsArray[ 0 ] ).toEqual( + '## [1.1.0](https://github.com/ckeditor/ckeditor5-package/compare/v1.0.0...v1.1.0) (0000-00-00)' + ); + expect( changesAsArray[ 1 ] ).toEqual( + '### MAJOR BREAKING CHANGES' + ); + expect( changesAsArray[ 2 ] ).toEqual( + '* This change should be scoped too but the script should work if the scope is being missed.' + ); + expect( changesAsArray[ 3 ] ).toEqual( + '### MINOR BREAKING CHANGES' + ); + expect( changesAsArray[ 4 ] ).toEqual( + '* **engine**: Nothing but I would like to use the note - engine.' + ); + expect( changesAsArray[ 5 ] ).toEqual( + '* **ui**: Nothing but I would like to use the note - ui.' + ); + expect( changesAsArray[ 6 ] ).toEqual( + '### Features' + ); + expect( changesAsArray[ 7 ] ).toEqual( + '* **autoformat**: It just works. ([commit](https://github.com/ckeditor/ckeditor5-package/c/bb))' + ); + expect( changesAsArray[ 8 ] ).toEqual( + '* **engine**: The first an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/c/xx))' + ); + expect( changesAsArray[ 9 ] ).toEqual( + '* The second an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/c/zz))' + ); + expect( changesAsArray[ 10 ] ).toEqual( + '### Bug fixes' + ); + expect( changesAsArray[ 11 ] ).toEqual( + '* **ui**: The first amazing bug fix. ([commit](https://github.com/ckeditor/ckeditor5-package/c/yy))' + ); + expect( changesAsArray[ 12 ] ).toEqual( + '### Other changes' + ); + expect( changesAsArray[ 13 ] ).toEqual( + '* Use the newest version of Node.js on CI. ([commit](https://github.com/ckeditor/ckeditor5-package/c/aa))' + ); } ); + } ); - const sortFunction = compareFunc( item => { - if ( Array.isArray( item.scope ) ) { - return item.scope[ 0 ]; - } - - // A hack that allows moving all non-scoped commits or breaking changes notes at the end of the list. - return 'z'.repeat( 15 ); - } ); - - options.commitsSort = sortFunction; - options.notesSort = sortFunction; - - return generateChangelog( commits, context, options ) - .then( changes => { - changes = replaceDates( changes ); + it( 'removes compare link from the header (context.skipCompareLink=true)', () => { + const context = { + version: '1.1.0', + repoUrl: url, + currentTag: 'v1.1.0', + previousTag: 'v1.0.0', + skipCompareLink: true + }; - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); + const options = getWriterOptions( transformCommitCallback( 7 ) ); - expect( changesAsArray[ 0 ] ).to.equal( - '## [1.1.0](https://github.com/ckeditor/ckeditor5-package/compare/v1.0.0...v1.1.0) (0000-00-00)' - ); - expect( changesAsArray[ 1 ] ).to.equal( - '### MAJOR BREAKING CHANGES' - ); - expect( changesAsArray[ 2 ] ).to.equal( - '* This change should be scoped too but the script should work if the scope is being missed.' - ); - expect( changesAsArray[ 3 ] ).to.equal( - '### MINOR BREAKING CHANGES' - ); - expect( changesAsArray[ 4 ] ).to.equal( - '* **engine**: Nothing but I would like to use the note - engine.' - ); - expect( changesAsArray[ 5 ] ).to.equal( - '* **ui**: Nothing but I would like to use the note - ui.' - ); - expect( changesAsArray[ 6 ] ).to.equal( - '### Features' - ); - expect( changesAsArray[ 7 ] ).to.equal( - '* **autoformat**: It just works. ([commit](https://github.com/ckeditor/ckeditor5-package/c/bb))' - ); - expect( changesAsArray[ 8 ] ).to.equal( - '* **engine**: The first an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/c/xx))' - ); - expect( changesAsArray[ 9 ] ).to.equal( - '* The second an amazing feature. ([commit](https://github.com/ckeditor/ckeditor5-package/c/zz))' - ); - expect( changesAsArray[ 10 ] ).to.equal( - '### Bug fixes' - ); - expect( changesAsArray[ 11 ] ).to.equal( - '* **ui**: The first amazing bug fix. ([commit](https://github.com/ckeditor/ckeditor5-package/c/yy))' - ); - expect( changesAsArray[ 12 ] ).to.equal( - '### Other changes' - ); - expect( changesAsArray[ 13 ] ).to.equal( - '* Use the newest version of Node.js on CI. ([commit](https://github.com/ckeditor/ckeditor5-package/c/aa))' - ); - } ); - } ); + return generateChangelog( [], context, options ) + .then( changes => { + changes = replaceDates( changes ); - it( 'removes compare link from the header (context.skipCompareLink=true)', () => { - const context = { - version: '1.1.0', - repoUrl: url, - currentTag: 'v1.1.0', - previousTag: 'v1.0.0', - skipCompareLink: true - }; + const changesAsArray = changes.split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.length ); - const options = getWriterOptions( { - hash: hash => hash.slice( 0, 7 ) + expect( changesAsArray[ 0 ] ).toEqual( + '## 1.1.0 (0000-00-00)' + ); } ); - - return generateChangelog( [], context, options ) - .then( changes => { - changes = replaceDates( changes ); - - const changesAsArray = changes.split( '\n' ) - .map( line => line.trim() ) - .filter( line => line.length ); - - expect( changesAsArray[ 0 ] ).to.equal( - '## 1.1.0 (0000-00-00)' - ); - } ); - } ); } ); } ); - - // Replaces dates to known string. It allows comparing changelog entries to strings - // which don't depend on the date. - function replaceDates( changelog ) { - return changelog.replace( /\d{4}-\d{2}-\d{2}/g, '0000-00-00' ); - } } ); + +// Replaces dates to known string. It allows comparing changelog entries to strings +// which don't depend on the date. +function replaceDates( changelog ) { + return changelog.replace( /\d{4}-\d{2}-\d{2}/g, '0000-00-00' ); +} + +/** + * @param {number} length + * @returns {WriterOptionsTransformCallback} + */ +function transformCommitCallback( length ) { + return commit => ( { + ...commit, + hash: commit.hash.slice( 0, length ) + } ); +} diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/getchangedfilesforcommit.js b/packages/ckeditor5-dev-release-tools/tests/utils/getchangedfilesforcommit.js index f9ef54978..e5d1efd32 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/getchangedfilesforcommit.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/getchangedfilesforcommit.js @@ -3,164 +3,161 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const expect = require( 'chai' ).expect; -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); - -describe( 'dev-release-tools/utils/transform-commit', () => { - let tmpCwd, cwd, getChangedFilesForCommit; - - describe( 'getChangedFilesForCommit()', function() { - this.timeout( 15 * 1000 ); - - before( () => { - cwd = process.cwd(); - tmpCwd = fs.mkdtempSync( __dirname + path.sep ); - process.chdir( tmpCwd ); - } ); - - after( () => { - process.chdir( cwd ); - fs.rmdirSync( tmpCwd ); - } ); - - beforeEach( () => { - exec( 'git init' ); - - if ( process.env.CI ) { - exec( 'git config user.email "ckeditor5@ckeditor.com"' ); - exec( 'git config user.name "CKEditor5 CI"' ); - } - - getChangedFilesForCommit = require( '../../lib/utils/getchangedfilesforcommit' ); - } ); - - afterEach( () => { - fs.rmSync( path.join( tmpCwd, '.git' ), { recursive: true } ); - fs.readdirSync( tmpCwd ).forEach( file => fs.unlinkSync( file ) ); - } ); - - it( 'returns files for initial commit', () => { - fs.writeFileSync( '1.txt', '' ); - fs.writeFileSync( '2.txt', '' ); - fs.writeFileSync( '3.txt', '' ); - fs.writeFileSync( '4.txt', '' ); - fs.writeFileSync( '5.txt', '' ); - exec( 'git add *.txt' ); - exec( 'git commit -m "Initial commit."' ); - - const files = getChangedFilesForCommit( getLastCommit() ); - - expect( files ).to.deep.equal( [ - '1.txt', - '2.txt', - '3.txt', - '4.txt', - '5.txt' - ] ); - } ); - - it( 'returns files for next commit after initial', () => { - fs.writeFileSync( '1.txt', '' ); - fs.writeFileSync( '2.txt', '' ); - fs.writeFileSync( '3.txt', '' ); - fs.writeFileSync( '4.txt', '' ); - fs.writeFileSync( '5.txt', '' ); - exec( 'git add *.txt' ); - exec( 'git commit -m "Initial commit."' ); - - fs.writeFileSync( '2.js', '' ); - fs.writeFileSync( '3.js', '' ); - fs.writeFileSync( '4.js', '' ); - exec( 'git add *.js' ); - exec( 'git commit -m "Next commit after initial."' ); - - const files = getChangedFilesForCommit( getLastCommit() ); - - expect( files ).to.deep.equal( [ - '2.js', - '3.js', - '4.js' - ] ); - } ); - - it( 'returns files for commit on new branch', () => { - fs.writeFileSync( '1.txt', '' ); - fs.writeFileSync( '2.txt', '' ); - fs.writeFileSync( '3.txt', '' ); - fs.writeFileSync( '4.txt', '' ); - fs.writeFileSync( '5.txt', '' ); - exec( 'git add *.txt' ); - exec( 'git commit -m "Initial commit."' ); - - fs.writeFileSync( '2.js', '' ); - fs.writeFileSync( '3.js', '' ); - fs.writeFileSync( '4.js', '' ); - exec( 'git add *.js' ); - exec( 'git commit -m "Next commit after initial."' ); - - exec( 'git checkout -b develop' ); - fs.writeFileSync( '5.json', '' ); - fs.writeFileSync( '6.json', '' ); - exec( 'git add *.json' ); - exec( 'git commit -m "New commit on branch develop."' ); - - const files = getChangedFilesForCommit( getLastCommit() ); - - expect( files ).to.deep.equal( [ - '5.json', - '6.json' - ] ); - } ); - - it( 'returns files for merge commit', () => { - fs.writeFileSync( '1.txt', '' ); - fs.writeFileSync( '2.txt', '' ); - fs.writeFileSync( '3.txt', '' ); - fs.writeFileSync( '4.txt', '' ); - fs.writeFileSync( '5.txt', '' ); - exec( 'git add *.txt' ); - exec( 'git commit -m "Initial commit."' ); - - fs.writeFileSync( '2.js', '' ); - fs.writeFileSync( '3.js', '' ); - fs.writeFileSync( '4.js', '' ); - exec( 'git add *.js' ); - exec( 'git commit -m "Next commit after initial."' ); - - exec( 'git checkout -b develop' ); - fs.writeFileSync( '5.json', '' ); - fs.writeFileSync( '6.json', '' ); - exec( 'git add *.json' ); - exec( 'git commit -m "New commit on branch develop."' ); - - exec( 'git checkout master' ); - fs.writeFileSync( '10.sh', '' ); - fs.writeFileSync( '11.sh', '' ); - fs.writeFileSync( '12.sh', '' ); - exec( 'git add *.sh' ); - exec( 'git commit -m "New commit on branch master."' ); - - exec( 'git merge develop' ); - exec( 'git branch -d develop' ); - - const files = getChangedFilesForCommit( getLastCommit() ); - - expect( files ).to.deep.equal( [ - '5.json', - '6.json' - ] ); - } ); +import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; +import path from 'path'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import getChangedFilesForCommit from '../../lib/utils/getchangedfilesforcommit.js'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); + +describe( 'getChangedFilesForCommit()', { timeout: 15000 }, function() { + let tmpCwd, cwd; + + beforeAll( () => { + cwd = process.cwd(); + tmpCwd = fs.mkdtempSync( path.join( __dirname, '..', 'test-fixtures' ) + path.sep ); + process.chdir( tmpCwd ); } ); - function exec( command ) { - return tools.shExec( command, { verbosity: 'error' } ); - } + afterAll( () => { + process.chdir( cwd ); + fs.rmdirSync( tmpCwd ); + } ); + + beforeEach( () => { + exec( 'git init' ); + + if ( process.env.CI ) { + exec( 'git config user.email "ckeditor5@ckeditor.com"' ); + exec( 'git config user.name "CKEditor5 CI"' ); + } + } ); + + afterEach( () => { + fs.rmSync( path.join( tmpCwd, '.git' ), { recursive: true } ); + fs.readdirSync( tmpCwd ).forEach( file => fs.unlinkSync( file ) ); + } ); + + it( 'returns files for initial commit', () => { + fs.writeFileSync( '1.txt', '' ); + fs.writeFileSync( '2.txt', '' ); + fs.writeFileSync( '3.txt', '' ); + fs.writeFileSync( '4.txt', '' ); + fs.writeFileSync( '5.txt', '' ); + exec( 'git add *.txt' ); + exec( 'git commit -m "Initial commit."' ); + + const files = getChangedFilesForCommit( getLastCommit() ); + + expect( files ).toEqual( [ + '1.txt', + '2.txt', + '3.txt', + '4.txt', + '5.txt' + ] ); + } ); - function getLastCommit() { - return exec( 'git log -n 1 --pretty=format:"%H"' ).trim(); - } + it( 'returns files for next commit after initial', () => { + fs.writeFileSync( '1.txt', '' ); + fs.writeFileSync( '2.txt', '' ); + fs.writeFileSync( '3.txt', '' ); + fs.writeFileSync( '4.txt', '' ); + fs.writeFileSync( '5.txt', '' ); + exec( 'git add *.txt' ); + exec( 'git commit -m "Initial commit."' ); + + fs.writeFileSync( '2.js', '' ); + fs.writeFileSync( '3.js', '' ); + fs.writeFileSync( '4.js', '' ); + exec( 'git add *.js' ); + exec( 'git commit -m "Next commit after initial."' ); + + const files = getChangedFilesForCommit( getLastCommit() ); + + expect( files ).toEqual( [ + '2.js', + '3.js', + '4.js' + ] ); + } ); + + it( 'returns files for commit on new branch', () => { + fs.writeFileSync( '1.txt', '' ); + fs.writeFileSync( '2.txt', '' ); + fs.writeFileSync( '3.txt', '' ); + fs.writeFileSync( '4.txt', '' ); + fs.writeFileSync( '5.txt', '' ); + exec( 'git add *.txt' ); + exec( 'git commit -m "Initial commit."' ); + + fs.writeFileSync( '2.js', '' ); + fs.writeFileSync( '3.js', '' ); + fs.writeFileSync( '4.js', '' ); + exec( 'git add *.js' ); + exec( 'git commit -m "Next commit after initial."' ); + + exec( 'git checkout -b develop' ); + fs.writeFileSync( '5.json', '' ); + fs.writeFileSync( '6.json', '' ); + exec( 'git add *.json' ); + exec( 'git commit -m "New commit on branch develop."' ); + + const files = getChangedFilesForCommit( getLastCommit() ); + + expect( files ).toEqual( [ + '5.json', + '6.json' + ] ); + } ); + + it( 'returns files for merge commit', () => { + fs.writeFileSync( '1.txt', '' ); + fs.writeFileSync( '2.txt', '' ); + fs.writeFileSync( '3.txt', '' ); + fs.writeFileSync( '4.txt', '' ); + fs.writeFileSync( '5.txt', '' ); + exec( 'git add *.txt' ); + exec( 'git commit -m "Initial commit."' ); + + fs.writeFileSync( '2.js', '' ); + fs.writeFileSync( '3.js', '' ); + fs.writeFileSync( '4.js', '' ); + exec( 'git add *.js' ); + exec( 'git commit -m "Next commit after initial."' ); + + exec( 'git checkout -b develop' ); + fs.writeFileSync( '5.json', '' ); + fs.writeFileSync( '6.json', '' ); + exec( 'git add *.json' ); + exec( 'git commit -m "New commit on branch develop."' ); + + exec( 'git checkout master' ); + fs.writeFileSync( '10.sh', '' ); + fs.writeFileSync( '11.sh', '' ); + fs.writeFileSync( '12.sh', '' ); + exec( 'git add *.sh' ); + exec( 'git commit -m "New commit on branch master."' ); + + exec( 'git merge develop' ); + exec( 'git branch -d develop' ); + + const files = getChangedFilesForCommit( getLastCommit() ); + + expect( files ).toEqual( [ + '5.json', + '6.json' + ] ); + } ); } ); + +function exec( command ) { + return tools.shExec( command, { verbosity: 'error' } ); +} + +function getLastCommit() { + return exec( 'git log -n 1 --pretty=format:"%H"' ).trim(); +} diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/getchangelog.js b/packages/ckeditor5-dev-release-tools/tests/utils/getchangelog.js new file mode 100644 index 000000000..9d3d70944 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/getchangelog.js @@ -0,0 +1,49 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import fs from 'fs'; +import path from 'path'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import getChangelog from '../../lib/utils/getchangelog.js'; + +vi.mock( 'fs' ); +vi.mock( 'path', () => ( { + default: { + join: vi.fn() + } +} ) ); +vi.mock( '../../lib/utils/constants.js', () => ( { + CHANGELOG_FILE: 'changelog.md' +} ) ); + +describe( 'getChangelog()', () => { + beforeEach( () => { + vi.mocked( path ).join.mockReturnValue( 'path-to-changelog' ); + vi.mocked( fs ).readFileSync.mockReturnValue( 'Content.' ); + } ); + + it( 'resolves the changelog content when a file exists (using default cwd)', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/home/ckeditor' ); + vi.mocked( fs ).existsSync.mockReturnValue( true ); + + expect( getChangelog() ).to.equal( 'Content.' ); + expect( vi.mocked( path ).join ).toHaveBeenCalledExactlyOnceWith( '/home/ckeditor', 'changelog.md' ); + expect( vi.mocked( fs ).readFileSync ).toHaveBeenCalledExactlyOnceWith( 'path-to-changelog', 'utf-8' ); + } ); + + it( 'resolves the changelog content when a file exists (using the specified cwd)', () => { + vi.mocked( fs ).existsSync.mockReturnValue( true ); + + expect( getChangelog( 'custom-cwd' ) ).to.equal( 'Content.' ); + expect( vi.mocked( path ).join ).toHaveBeenCalledExactlyOnceWith( 'custom-cwd', 'changelog.md' ); + expect( vi.mocked( fs ).readFileSync ).toHaveBeenCalledExactlyOnceWith( 'path-to-changelog', 'utf-8' ); + } ); + + it( 'returns null if the changelog does not exist', () => { + vi.mocked( fs ).existsSync.mockReturnValue( false ); + + expect( getChangelog() ).to.equal( null ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/getchangesforversion.js b/packages/ckeditor5-dev-release-tools/tests/utils/getchangesforversion.js new file mode 100644 index 000000000..c0c4d58ce --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/getchangesforversion.js @@ -0,0 +1,263 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import getChangelog from '../../lib/utils/getchangelog.js'; +import getChangesForVersion from '../../lib/utils/getchangesforversion.js'; +import { CHANGELOG_HEADER } from '../../lib/utils/constants.js'; + +vi.mock( '../../lib/utils/getchangelog.js' ); + +describe( 'getChangesForVersion()', () => { + beforeEach( () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/home/ckeditor' ); + } ); + + it( 'returns changes for the first tag which is a link to the release (default cwd)', () => { + const expectedChangelog = [ + '### Features', + '', + '* Cloned the main module. ([abcd123](https://github.com))' + ].join( '\n' ); + + const changelog = [ + '## [0.1.0](https://github.com) (2017-01-13)', + '', + expectedChangelog + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( '0.1.0' ) ).to.equal( expectedChangelog ); + expect( getChangelog ).toHaveBeenCalledExactlyOnceWith( '/home/ckeditor' ); + } ); + + it( 'returns changes for the first tag which is a link to the release (a custom cwd)', () => { + const expectedChangelog = [ + '### Features', + '', + '* Cloned the main module. ([abcd123](https://github.com))' + ].join( '\n' ); + + const changelog = [ + '## [0.1.0](https://github.com) (2017-01-13)', + '', + expectedChangelog + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( '0.1.0', '/custom/cwd' ) ).to.equal( expectedChangelog ); + expect( getChangelog ).toHaveBeenCalledExactlyOnceWith( '/custom/cwd' ); + } ); + + it( 'returns changes if a specified version starts with the "v" letter', () => { + const expectedChangelog = [ + '### Features', + '', + '* Cloned the main module. ([abcd123](https://github.com))' + ].join( '\n' ); + + const changelog = [ + '## 0.1.0 (2017-01-13)', + '', + expectedChangelog + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( 'v0.1.0' ) ).to.equal( expectedChangelog ); + } ); + + it( 'returns changes for the first tag which is not a link', () => { + const expectedChangelog = [ + '### Features', + '', + '* Cloned the main module. ([abcd123](https://github.com))' + ].join( '\n' ); + + const changelog = [ + '## 0.1.0 (2017-01-13)', + '', + expectedChangelog + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( '0.1.0' ) ).to.equal( expectedChangelog ); + } ); + + it( 'returns changes between tags', () => { + const expectedChangelog = [ + '### Features', + '', + '* Cloned the main module. ([abcd123](https://github.com))', + '', + '### BREAKING CHANGE', + '', + '* Bump the major!' + ].join( '\n' ); + + const changelog = [ + '## [1.0.0](https://github.com/) (2017-01-13)', + '', + expectedChangelog, + '', + '## [0.1.0](https://github.com) (2017-01-13)', + '', + '### Features', + '', + '* Cloned the main module. ([abcd123](https://github.com))' + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( '1.0.0' ) ).to.equal( expectedChangelog ); + } ); + + it( 'returns null if cannot find changes for the specified version', () => { + const changelog = [ + '## [0.1.0](https://github.com) (2017-01-13)', + '', + '### Features', + '', + '* Cloned the main module. ([abcd123](https://github.com))' + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + expect( getChangesForVersion( '1.0.0' ) ).to.equal( null ); + } ); + + it( 'works when date is not specified', () => { + const changelog = [ + '## 0.3.0', + '', + 'Foo' + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( '0.3.0' ) ).to.equal( 'Foo' ); + } ); + + it( 'captures correct range of changes (headers are URLs)', () => { + const changelog = [ + '## [0.3.0](https://github.com) (2017-01-13)', + '', + '3', + '', + 'Some text ## [like a release header]', + '', + '## [0.2.0](https://github.com) (2017-01-13)', + '', + '2', + '', + '## [0.1.0](https://github.com) (2017-01-13)', + '', + '1' + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( '0.3.0' ) ).to.equal( '3\n\nSome text ## [like a release header]' ); + expect( getChangesForVersion( '0.2.0' ) ).to.equal( '2' ); + } ); + + it( 'captures correct range of changes (headers are plain text, "the initial" release check)', () => { + const changelog = [ + 'Changelog', + '=========', + '', + '## 1.0.2 (2022-02-22)', + '', + '### Other changes', + '', + '* Other change for `1.0.2`.', + '', + '', + '## 1.0.1 (2022-02-22)', + '', + '### Other changes', + '', + '* Other change for `1.0.1`.', + '', + '', + '## 1.0.0 (2022-02-22)', + '', + 'This is the initial release.' + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( '1.0.0' ) ).to.equal( 'This is the initial release.' ); + } ); + + it( 'captures correct range of changes (headers are plain text, "middle" version check)', () => { + const changelog = [ + 'Changelog', + '=========', + '', + '## 1.0.2 (2022-02-22)', + '', + '### Other changes', + '', + '* Other change for `1.0.2`.', + '', + '', + '## 1.0.1 (2022-02-22)', + '', + '### Other changes', + '', + '* Other change for `1.0.1`.', + '', + '', + '## 1.0.0 (2022-02-22)', + '', + 'This is the initial release.' + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( '1.0.1' ) ).to.equal( [ + '### Other changes', + '', + '* Other change for `1.0.1`.' + ].join( '\n' ) ); + } ); + + it( 'captures correct range of changes (headers are plain text, "the latest" check)', () => { + const changelog = [ + 'Changelog', + '=========', + '', + '## 1.0.2 (2022-02-22)', + '', + '### Other changes', + '', + '* Other change for `1.0.2`.', + '', + '', + '## 1.0.1 (2022-02-22)', + '', + '### Other changes', + '', + '* Other change for `1.0.1`.', + '', + '', + '## 1.0.0 (2022-02-22)', + '', + 'This is the initial release.' + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelog ); + + expect( getChangesForVersion( '1.0.2' ) ).to.equal( [ + '### Other changes', + '', + '* Other change for `1.0.2`.' + ].join( '\n' ) ); + } ); +} ); + diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/getcommits.js b/packages/ckeditor5-dev-release-tools/tests/utils/getcommits.js index 56f8e805d..8b0824d66 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/getcommits.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/getcommits.js @@ -3,319 +3,304 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const sinon = require( 'sinon' ); -const expect = require( 'chai' ).expect; -const mockery = require( 'mockery' ); -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); - -describe( 'dev-release-tools/utils', () => { - let tmpCwd, cwd, stubs, sandbox, getCommits; - - describe( 'getCommits()', () => { - before( () => { - cwd = process.cwd(); - tmpCwd = fs.mkdtempSync( __dirname + path.sep ); - } ); +import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; +import path from 'path'; +import { getRawCommitsStream } from 'git-raw-commits'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import shellEscape from 'shell-escape'; - after( () => { - fs.rmdirSync( tmpCwd ); - } ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); + +vi.mock( 'shell-escape' ); - beforeEach( () => { - process.chdir( tmpCwd ); +describe( 'getCommits()', () => { + let tmpCwd, cwd, getCommits, stubs; - exec( 'git init' ); + beforeAll( () => { + cwd = process.cwd(); + tmpCwd = fs.mkdtempSync( path.join( __dirname, '..', 'test-fixtures' ) + path.sep ); + } ); - if ( process.env.CI ) { - exec( 'git config user.email "ckeditor5@ckeditor.com"' ); - exec( 'git config user.name "CKEditor5 CI"' ); - } + afterAll( () => { + fs.rmdirSync( tmpCwd ); + } ); - sandbox = sinon.createSandbox(); + beforeEach( async () => { + process.chdir( tmpCwd ); - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); + exec( 'git init' ); - stubs = { - shExec: sandbox.stub(), - gitRawCommits: sandbox.stub() - }; + if ( process.env.CI ) { + exec( 'git config user.email "ckeditor5@ckeditor.com"' ); + exec( 'git config user.name "CKEditor5 CI"' ); + } - // Other kinds of mocking the `git-raw-commits` package end with an error. - // But this way it works. - stubs.gitRawCommits.callsFake( options => { - const modulePath = require.resolve( 'git-raw-commits' ); + vi.mocked( shellEscape ).mockImplementation( input => input.map( v => `'${ v }'` ).join( ' ' ) ); - return require( modulePath )( options ); - } ); + vi.doMock( 'git-raw-commits', () => ( { + getRawCommitsStream: vi.fn( getRawCommitsStream ) + } ) ); - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { - tools: { - shExec: stubs.shExec - } - } ); + vi.doMock( '@ckeditor/ckeditor5-dev-utils' ); - mockery.registerMock( 'git-raw-commits', stubs.gitRawCommits ); + stubs = { + getRawCommitsStream: ( await import( 'git-raw-commits' ) ).getRawCommitsStream, + devTools: ( await import( '@ckeditor/ckeditor5-dev-utils' ) ).tools + }; - getCommits = require( '../../lib/utils/getcommits' ); + getCommits = ( await import( '../../lib/utils/getcommits.js' ) ).default; + } ); + + afterEach( () => { + process.chdir( cwd ); + fs.rmSync( path.join( tmpCwd, '.git' ), { recursive: true } ); + + vi.resetModules(); + } ); + + describe( 'branch for releasing is the same as the main branch', () => { + beforeEach( async () => { + vi.mocked( stubs.devTools.shExec ).mockReturnValueOnce( 'master\n' ); } ); - afterEach( () => { - process.chdir( cwd ); - fs.rmSync( path.join( tmpCwd, '.git' ), { recursive: true } ); + it( 'throws an error when the specified release branch is not equal to the current checked out branch', () => { + return getCommits( transformCommit, { releaseBranch: 'release' } ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + err => { + expect( err.message ).toEqual( + 'Expected to be checked out on the release branch ("release") instead of "master". Aborting.' + ); + } + ); + } ); - sandbox.restore(); - mockery.disable(); + it( 'throws an error when the default release branch is not equal to the current checked out branch', () => { + vi.mocked( stubs.devTools.shExec ).mockReset(); + vi.mocked( stubs.devTools.shExec ).mockReturnValueOnce( 'release\n' ); + + return getCommits( transformCommit ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + err => { + expect( err.message ).toEqual( + 'Expected to be checked out on the release branch ("master") instead of "release". Aborting.' + ); + } + ); } ); - describe( 'branch for releasing is the same as the main branch', () => { - beforeEach( () => { - stubs.shExec.onFirstCall().returns( 'master\n' ); - } ); - - it( 'throws an error when the specified release branch is not equal to the current checked out branch', () => { - return getCommits( transformCommit, { releaseBranch: 'release' } ) - .then( - () => { - throw new Error( 'Supposed to be rejected.' ); - }, - err => { - expect( err.message ).to.equal( - 'Expected to be checked out on the release branch ("release") instead of "master". Aborting.' - ); - } - ); - } ); - - it( 'throws an error when the default release branch is not equal to the current checked out branch', () => { - stubs.shExec.reset(); - stubs.shExec.onFirstCall().returns( 'release\n' ); - - return getCommits( transformCommit ) - .then( - () => { - throw new Error( 'Supposed to be rejected.' ); - }, - err => { - expect( err.message ).to.equal( - 'Expected to be checked out on the release branch ("master") instead of "release". Aborting.' - ); - } - ); - } ); - - it( 'throws an error when repository is empty', () => { - return getCommits( transformCommit ) - .then( - () => { - throw new Error( 'Supposed to be rejected.' ); - }, - err => { - expect( err.message ).to.equal( 'Given repository is empty.' ); - } - ); - } ); - - it( 'throws an error when there is no tag or commit with specified name in given repository', () => { - return getCommits( transformCommit, { from: 'foobar' } ) - .then( - () => { - throw new Error( 'Supposed to be rejected.' ); - }, - err => { - expect( err.message ).to.equal( 'Cannot find tag or commit "foobar" in given repository.' ); - } - ); - } ); - - it( 'returns an array of commits after "git init"', () => { - exec( 'git commit --allow-empty --message "First."' ); - exec( 'git commit --allow-empty --message "Second."' ); - - return getCommits( transformCommit ) - .then( commits => { - expect( commits.length ).to.equal( 2 ); - expect( commits[ 0 ].header ).to.equal( 'Second.' ); - expect( commits[ 1 ].header ).to.equal( 'First.' ); - } ); - } ); + it( 'throws an error when repository is empty', () => { + return getCommits( transformCommit ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + err => { + expect( err.message ).toEqual( 'Given repository is empty.' ); + } + ); + } ); - it( 'returns an array of commits after "git init" (main branch is not equal to "master")', () => { - stubs.shExec.reset(); - stubs.shExec.onFirstCall().returns( 'main-branch\n' ); + it( 'throws an error when there is no tag or commit with specified name in given repository', () => { + return getCommits( transformCommit, { from: 'foobar' } ) + .then( + () => { + throw new Error( 'Supposed to be rejected.' ); + }, + err => { + expect( err.message ).toEqual( 'Cannot find tag or commit "foobar" in given repository.' ); + } + ); + } ); - exec( 'git checkout -b main-branch' ); - exec( 'git commit --allow-empty --message "First."' ); - exec( 'git commit --allow-empty --message "Second."' ); + it( 'returns an array of commits after "git init"', () => { + exec( 'git commit --allow-empty --message "First."' ); + exec( 'git commit --allow-empty --message "Second."' ); - return getCommits( transformCommit, { mainBranch: 'main-branch', releaseBranch: 'main-branch' } ) - .then( commits => { - expect( commits.length ).to.equal( 2 ); - expect( commits[ 0 ].header ).to.equal( 'Second.' ); - expect( commits[ 1 ].header ).to.equal( 'First.' ); - } ); - } ); - - it( 'returns an array of commits after "git init" if `options.from` is not specified', () => { - exec( 'git commit --allow-empty --message "First."' ); - exec( 'git commit --allow-empty --message "Second."' ); - exec( 'git tag v1.0.0' ); - exec( 'git commit --allow-empty --message "Third."' ); - exec( 'git commit --allow-empty --message "Fourth."' ); - - return getCommits( transformCommit ) - .then( commits => { - expect( commits.length ).to.equal( 4 ); - expect( commits[ 0 ].header ).to.equal( 'Fourth.' ); - expect( commits[ 1 ].header ).to.equal( 'Third.' ); - expect( commits[ 2 ].header ).to.equal( 'Second.' ); - expect( commits[ 3 ].header ).to.equal( 'First.' ); - } ); - } ); - - it( 'returns an array of commits since last tag (`options.from` is specified)', () => { - exec( 'git commit --allow-empty --message "First."' ); - exec( 'git commit --allow-empty --message "Second."' ); - exec( 'git tag v1.0.0' ); - exec( 'git commit --allow-empty --message "Third."' ); - exec( 'git commit --allow-empty --message "Fourth."' ); - - return getCommits( transformCommit, { from: 'v1.0.0' } ) - .then( commits => { - expect( commits.length ).to.equal( 2 ); - expect( commits[ 0 ].header ).to.equal( 'Fourth.' ); - expect( commits[ 1 ].header ).to.equal( 'Third.' ); - } ); - } ); + return getCommits( transformCommit ) + .then( commits => { + expect( commits.length ).toEqual( 2 ); + expect( commits[ 0 ].header ).toEqual( 'Second.' ); + expect( commits[ 1 ].header ).toEqual( 'First.' ); + } ); + } ); - it( 'returns an array of commits since specified commit (`options.from` is specified)', () => { - exec( 'git commit --allow-empty --message "First."' ); - exec( 'git commit --allow-empty --message "Second."' ); - exec( 'git tag v1.0.0' ); - exec( 'git commit --allow-empty --message "Third."' ); + it( 'returns an array of commits after "git init" (main branch is not equal to "master")', () => { + vi.mocked( stubs.devTools.shExec ).mockReset(); + vi.mocked( stubs.devTools.shExec ).mockReturnValueOnce( 'main-branch\n' ); - const commitId = exec( 'git rev-parse HEAD' ).trim(); + exec( 'git checkout -b main-branch' ); + exec( 'git commit --allow-empty --message "First."' ); + exec( 'git commit --allow-empty --message "Second."' ); - exec( 'git commit --allow-empty --message "Fourth."' ); + return getCommits( transformCommit, { mainBranch: 'main-branch', releaseBranch: 'main-branch' } ) + .then( commits => { + expect( commits.length ).toEqual( 2 ); + expect( commits[ 0 ].header ).toEqual( 'Second.' ); + expect( commits[ 1 ].header ).toEqual( 'First.' ); + } ); + } ); - return getCommits( transformCommit, { from: commitId } ) - .then( commits => { - expect( commits.length ).to.equal( 1 ); - expect( commits[ 0 ].header ).to.equal( 'Fourth.' ); - } ); - } ); + it( 'returns an array of commits after "git init" if `options.from` is not specified', () => { + exec( 'git commit --allow-empty --message "First."' ); + exec( 'git commit --allow-empty --message "Second."' ); + exec( 'git tag v1.0.0' ); + exec( 'git commit --allow-empty --message "Third."' ); + exec( 'git commit --allow-empty --message "Fourth."' ); + + return getCommits( transformCommit ) + .then( commits => { + expect( commits.length ).toEqual( 4 ); + expect( commits[ 0 ].header ).toEqual( 'Fourth.' ); + expect( commits[ 1 ].header ).toEqual( 'Third.' ); + expect( commits[ 2 ].header ).toEqual( 'Second.' ); + expect( commits[ 3 ].header ).toEqual( 'First.' ); + } ); + } ); - it( 'ignores false values returned by the "transformCommit" mapper', () => { - const transformCommit = sinon.stub(); + it( 'returns an array of commits since last tag (`options.from` is specified)', () => { + exec( 'git commit --allow-empty --message "First."' ); + exec( 'git commit --allow-empty --message "Second."' ); + exec( 'git tag v1.0.0' ); + exec( 'git commit --allow-empty --message "Third."' ); + exec( 'git commit --allow-empty --message "Fourth."' ); + + return getCommits( transformCommit, { from: 'v1.0.0' } ) + .then( commits => { + expect( commits.length ).toEqual( 2 ); + expect( commits[ 0 ].header ).toEqual( 'Fourth.' ); + expect( commits[ 1 ].header ).toEqual( 'Third.' ); + } ); + } ); - transformCommit.onFirstCall().callsFake( commit => commit ); - transformCommit.onSecondCall().callsFake( () => null ); + it( 'returns an array of commits since specified commit (`options.from` is specified)', () => { + exec( 'git commit --allow-empty --message "First."' ); + exec( 'git commit --allow-empty --message "Second."' ); + exec( 'git tag v1.0.0' ); + exec( 'git commit --allow-empty --message "Third."' ); - exec( 'git commit --allow-empty --message "First."' ); - exec( 'git commit --allow-empty --message "Second."' ); + const commitId = exec( 'git rev-parse HEAD' ).trim(); - return getCommits( transformCommit ) - .then( commits => { - expect( commits.length ).to.equal( 1 ); - expect( commits[ 0 ].header ).to.equal( 'Second.' ); - } ); - } ); + exec( 'git commit --allow-empty --message "Fourth."' ); - it( 'handles arrays returned by the "transformCommit" mapper', () => { - const transformCommit = sinon.stub(); + return getCommits( transformCommit, { from: commitId } ) + .then( commits => { + expect( commits.length ).toEqual( 1 ); + expect( commits[ 0 ].header ).toEqual( 'Fourth.' ); + } ); + } ); + + it( 'ignores false values returned by the "transformCommit" mapper', () => { + const transformCommit = vi.fn() + .mockImplementationOnce( commit => commit ) + .mockImplementationOnce( () => null ); + + exec( 'git commit --allow-empty --message "First."' ); + exec( 'git commit --allow-empty --message "Second."' ); - transformCommit.onFirstCall().callsFake( commit => { + return getCommits( transformCommit ) + .then( commits => { + expect( commits.length ).toEqual( 1 ); + expect( commits[ 0 ].header ).toEqual( 'Second.' ); + } ); + } ); + + it( 'handles arrays returned by the "transformCommit" mapper', () => { + const transformCommit = vi.fn() + .mockImplementationOnce( commit => { return [ commit, commit ]; } ); - exec( 'git commit --allow-empty --message "First."' ); + exec( 'git commit --allow-empty --message "First."' ); - return getCommits( transformCommit ) - .then( commits => { - expect( commits.length ).to.equal( 2 ); - expect( commits[ 0 ].header ).to.equal( 'First.' ); - expect( commits[ 1 ].header ).to.equal( 'First.' ); - } ); - } ); + return getCommits( transformCommit ) + .then( commits => { + expect( commits.length ).toEqual( 2 ); + expect( commits[ 0 ].header ).toEqual( 'First.' ); + expect( commits[ 1 ].header ).toEqual( 'First.' ); + } ); } ); + } ); + + describe( 'branch for releasing is other than the main branch', () => { + it( 'collects commits from the main branch and the release branch', () => { + vi.mocked( stubs.devTools.shExec ).mockReturnValueOnce( 'release\n' ); + vi.mocked( stubs.devTools.shExec ).mockImplementationOnce( exec ); + + exec( 'git commit --allow-empty --message "Type: master: 1."' ); + exec( 'git tag v1.0.0' ); + + // Commits on master and release branches will be parsed. + exec( 'git commit --allow-empty --message "Type: master: 2."' ); + exec( 'git commit --allow-empty --message "Type: master: 3."' ); + exec( 'git commit --allow-empty --message "Type: master: 4."' ); + + exec( 'git checkout -b i/100' ); + exec( 'git commit --allow-empty --message "Type: i/100: 1."' ); + exec( 'git commit --allow-empty --message "Type: i/100: 2."' ); + exec( 'git checkout master' ); + exec( 'git merge i/100 --no-ff --message "Type: Merge i/100. master: 5"' ); + + exec( 'git checkout -b i/200' ); + exec( 'git commit --allow-empty --message "Type: i/200: 1."' ); + exec( 'git commit --allow-empty --message "Type: i/200: 2."' ); + exec( 'git commit --allow-empty --message "Type: i/200: 3."' ); + exec( 'git checkout master' ); + exec( 'git merge i/200 --no-ff --message "Type: Merge i/200. master: 6"' ); + + const baseCommit = exec( 'git rev-parse HEAD' ).trim(); + + exec( 'git checkout -b release' ); + exec( 'git commit --allow-empty --message "Type: release: 1, master 7."' ); + + exec( 'git checkout -b i/300' ); + exec( 'git commit --allow-empty --message "Type: i/300: 1."' ); + exec( 'git commit --allow-empty --message "Type: i/300: 2."' ); + exec( 'git checkout release' ); + exec( 'git merge i/300 --no-ff --message "Type: Merge i/300. release: 2, master: 8"' ); + + exec( 'git commit --allow-empty --message "Type: release: 3, master 9."' ); + exec( 'git branch -D i/100 i/200 i/300' ); + + return getCommits( transformCommit, { from: 'v1.0.0', releaseBranch: 'release' } ) + .then( commits => { + expect( commits.length ).toEqual( 8 ); + + expect( stubs.getRawCommitsStream ).toHaveBeenNthCalledWith( 1, { + from: 'v1.0.0', + to: baseCommit, + format: '%B%n-hash-%n%H', + merges: undefined, + firstParent: true + } ); - describe( 'branch for releasing is other than the main branch', () => { - it( 'collects commits from the main branch and the release branch', () => { - stubs.shExec.onFirstCall().returns( 'release\n' ); - stubs.shExec.onSecondCall().callsFake( exec ); - - exec( 'git commit --allow-empty --message "Type: master: 1."' ); - exec( 'git tag v1.0.0' ); - - // Commits on master and release branches will be parsed. - exec( 'git commit --allow-empty --message "Type: master: 2."' ); - exec( 'git commit --allow-empty --message "Type: master: 3."' ); - exec( 'git commit --allow-empty --message "Type: master: 4."' ); - - exec( 'git checkout -b i/100' ); - exec( 'git commit --allow-empty --message "Type: i/100: 1."' ); - exec( 'git commit --allow-empty --message "Type: i/100: 2."' ); - exec( 'git checkout master' ); - exec( 'git merge i/100 --no-ff --message "Type: Merge i/100. master: 5"' ); - - exec( 'git checkout -b i/200' ); - exec( 'git commit --allow-empty --message "Type: i/200: 1."' ); - exec( 'git commit --allow-empty --message "Type: i/200: 2."' ); - exec( 'git commit --allow-empty --message "Type: i/200: 3."' ); - exec( 'git checkout master' ); - exec( 'git merge i/200 --no-ff --message "Type: Merge i/200. master: 6"' ); - - const baseCommit = exec( 'git rev-parse HEAD' ).trim(); - - exec( 'git checkout -b release' ); - exec( 'git commit --allow-empty --message "Type: release: 1, master 7."' ); - - exec( 'git checkout -b i/300' ); - exec( 'git commit --allow-empty --message "Type: i/300: 1."' ); - exec( 'git commit --allow-empty --message "Type: i/300: 2."' ); - exec( 'git checkout release' ); - exec( 'git merge i/300 --no-ff --message "Type: Merge i/300. release: 2, master: 8"' ); - - exec( 'git commit --allow-empty --message "Type: release: 3, master 9."' ); - exec( 'git branch -D i/100 i/200 i/300' ); - - return getCommits( transformCommit, { from: 'v1.0.0', releaseBranch: 'release' } ) - .then( commits => { - expect( commits.length ).to.equal( 8 ); - - expect( stubs.gitRawCommits.firstCall.args[ 0 ] ).to.deep.equal( { - from: 'v1.0.0', - to: baseCommit, - format: '%B%n-hash-%n%H', - merges: undefined, - firstParent: true - } ); - - expect( stubs.gitRawCommits.secondCall.args[ 0 ] ).to.deep.equal( { - to: 'HEAD', - from: baseCommit, - format: '%B%n-hash-%n%H', - merges: undefined, - firstParent: true - } ); + expect( stubs.getRawCommitsStream ).toHaveBeenNthCalledWith( 2, { + to: 'HEAD', + from: baseCommit, + format: '%B%n-hash-%n%H', + merges: undefined, + firstParent: true } ); - } ); + + expect( vi.mocked( shellEscape ) ).toHaveBeenCalledExactlyOnceWith( [ 'release', 'master' ] ); + } ); } ); } ); +} ); - function exec( command ) { - return tools.shExec( command, { verbosity: 'error' } ); - } +function exec( command ) { + return tools.shExec( command, { verbosity: 'error' } ); +} - // Do not modify the commit. - function transformCommit( commit ) { - return commit; - } -} ); +// Do not modify the commit. +function transformCommit( commit ) { + return commit; +} diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/getformatteddate.js b/packages/ckeditor5-dev-release-tools/tests/utils/getformatteddate.js new file mode 100644 index 000000000..e69387684 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/getformatteddate.js @@ -0,0 +1,22 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import getFormattedDate from '../../lib/utils/getformatteddate.js'; + +describe( 'getFormattedDate()', () => { + beforeEach( () => { + vi.useFakeTimers(); + vi.setSystemTime( new Date( '2023-06-15 12:00:00' ) ); + } ); + + afterEach( () => { + vi.useRealTimers(); + } ); + + it( 'returns a date following the format "year-month-day" with the leading zeros', () => { + expect( getFormattedDate() ).to.equal( '2023-06-15' ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/getnewversiontype.js b/packages/ckeditor5-dev-release-tools/tests/utils/getnewversiontype.js index 00c7b8dba..4f2303395 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/getnewversiontype.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/getnewversiontype.js @@ -3,117 +3,113 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect } from 'vitest'; +import getNewVersionType from '../../lib/utils/getnewversiontype.js'; -const expect = require( 'chai' ).expect; -const getNewVersionType = require( '../../lib/utils/getnewversiontype' ); - -describe( 'dev-release-tools/utils', () => { - describe( 'getSubPackagesPaths()', () => { - it( 'returns "skip" when passing an empty array of commits', () => { - expect( getNewVersionType( [] ) ).to.equal( 'skip' ); - } ); +describe( 'getSubPackagesPaths()', () => { + it( 'returns "skip" when passing an empty array of commits', () => { + expect( getNewVersionType( [] ) ).toEqual( 'skip' ); + } ); - it( 'returns "internal" when passing non-public commits', () => { - expect( getNewVersionType( [ { isPublicCommit: false } ] ) ).to.equal( 'internal' ); - } ); + it( 'returns "internal" when passing non-public commits', () => { + expect( getNewVersionType( [ { isPublicCommit: false } ] ) ).toEqual( 'internal' ); + } ); - it( 'returns "major" if MAJOR BREAKING CHANGES was introduced in "type:Other" commit', () => { - const commits = [ - { isPublicCommit: true, notes: [ { title: 'MAJOR BREAKING CHANGES' } ], rawType: 'Other' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'major' ); - } ); + it( 'returns "major" if MAJOR BREAKING CHANGES was introduced in "type:Other" commit', () => { + const commits = [ + { isPublicCommit: true, notes: [ { title: 'MAJOR BREAKING CHANGES' } ], rawType: 'Other' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'major' ); + } ); - it( 'returns "major" if BREAKING CHANGES was introduced in "type:Other" commit', () => { - const commits = [ - { isPublicCommit: true, notes: [ { title: 'BREAKING CHANGES' } ], rawType: 'Other' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'major' ); - } ); + it( 'returns "major" if BREAKING CHANGES was introduced in "type:Other" commit', () => { + const commits = [ + { isPublicCommit: true, notes: [ { title: 'BREAKING CHANGES' } ], rawType: 'Other' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'major' ); + } ); - it( 'returns "major" if MAJOR BREAKING CHANGES was introduced in "type:Fix" commit', () => { - const commits = [ - { isPublicCommit: true, notes: [ { title: 'MAJOR BREAKING CHANGES' } ], rawType: 'Fix' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'major' ); - } ); + it( 'returns "major" if MAJOR BREAKING CHANGES was introduced in "type:Fix" commit', () => { + const commits = [ + { isPublicCommit: true, notes: [ { title: 'MAJOR BREAKING CHANGES' } ], rawType: 'Fix' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'major' ); + } ); - it( 'returns "major" if BREAKING CHANGES was introduced in "type:Fix" commit', () => { - const commits = [ - { isPublicCommit: true, notes: [ { title: 'BREAKING CHANGES' } ], rawType: 'Fix' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'major' ); - } ); + it( 'returns "major" if BREAKING CHANGES was introduced in "type:Fix" commit', () => { + const commits = [ + { isPublicCommit: true, notes: [ { title: 'BREAKING CHANGES' } ], rawType: 'Fix' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'major' ); + } ); - it( 'returns "major" if MAJOR BREAKING CHANGES was introduced in "type:Feature" commit', () => { - const commits = [ - { isPublicCommit: true, notes: [ { title: 'MAJOR BREAKING CHANGES' } ], rawType: 'Feature' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'major' ); - } ); + it( 'returns "major" if MAJOR BREAKING CHANGES was introduced in "type:Feature" commit', () => { + const commits = [ + { isPublicCommit: true, notes: [ { title: 'MAJOR BREAKING CHANGES' } ], rawType: 'Feature' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'major' ); + } ); - it( 'returns "major" if BREAKING CHANGES was introduced in "type:Feature" commit', () => { - const commits = [ - { isPublicCommit: true, notes: [ { title: 'BREAKING CHANGES' } ], rawType: 'Feature' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'major' ); - } ); + it( 'returns "major" if BREAKING CHANGES was introduced in "type:Feature" commit', () => { + const commits = [ + { isPublicCommit: true, notes: [ { title: 'BREAKING CHANGES' } ], rawType: 'Feature' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'major' ); + } ); - it( 'returns "minor" if MINOR BREAKING CHANGES was introduced in "type:Other" commit', () => { - const commits = [ - { isPublicCommit: true, notes: [ { title: 'MINOR BREAKING CHANGES' } ], rawType: 'Other' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'minor' ); - } ); + it( 'returns "minor" if MINOR BREAKING CHANGES was introduced in "type:Other" commit', () => { + const commits = [ + { isPublicCommit: true, notes: [ { title: 'MINOR BREAKING CHANGES' } ], rawType: 'Other' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'minor' ); + } ); - it( 'returns "minor" if MINOR BREAKING CHANGES was introduced in "type:Fix" commit', () => { - const commits = [ - { isPublicCommit: true, notes: [ { title: 'MINOR BREAKING CHANGES' } ], rawType: 'Fix' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'minor' ); - } ); + it( 'returns "minor" if MINOR BREAKING CHANGES was introduced in "type:Fix" commit', () => { + const commits = [ + { isPublicCommit: true, notes: [ { title: 'MINOR BREAKING CHANGES' } ], rawType: 'Fix' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'minor' ); + } ); - it( 'returns "minor" if MINOR BREAKING CHANGES was introduced in "type:Feature" commit', () => { - const commits = [ - { isPublicCommit: true, notes: [ { title: 'MINOR BREAKING CHANGES' } ], rawType: 'Feature' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'minor' ); - } ); + it( 'returns "minor" if MINOR BREAKING CHANGES was introduced in "type:Feature" commit', () => { + const commits = [ + { isPublicCommit: true, notes: [ { title: 'MINOR BREAKING CHANGES' } ], rawType: 'Feature' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'minor' ); + } ); - it( 'returns "minor" if found "type:Feature" commit in the collection', () => { - const commits = [ - { isPublicCommit: true, notes: [], rawType: 'Fix' }, - { isPublicCommit: true, notes: [], rawType: 'Other' }, - { isPublicCommit: false, notes: [], rawType: 'Docs' }, - { isPublicCommit: true, notes: [], rawType: 'Feature' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'minor' ); - } ); + it( 'returns "minor" if found "type:Feature" commit in the collection', () => { + const commits = [ + { isPublicCommit: true, notes: [], rawType: 'Fix' }, + { isPublicCommit: true, notes: [], rawType: 'Other' }, + { isPublicCommit: false, notes: [], rawType: 'Docs' }, + { isPublicCommit: true, notes: [], rawType: 'Feature' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'minor' ); + } ); - it( 'returns "major" if found "MAJOR BREAKING CHANGES" commit in the collection', () => { - const commits = [ - { isPublicCommit: true, notes: [], rawType: 'Fix' }, - { isPublicCommit: true, notes: [], rawType: 'Other' }, - { isPublicCommit: false, notes: [], rawType: 'Docs' }, - { - isPublicCommit: true, - notes: [ - { title: 'MINOR BREAKING CHANGES' }, - { title: 'MAJOR BREAKING CHANGES' } - ], - rawType: 'Feature' - } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'major' ); - } ); + it( 'returns "major" if found "MAJOR BREAKING CHANGES" commit in the collection', () => { + const commits = [ + { isPublicCommit: true, notes: [], rawType: 'Fix' }, + { isPublicCommit: true, notes: [], rawType: 'Other' }, + { isPublicCommit: false, notes: [], rawType: 'Docs' }, + { + isPublicCommit: true, + notes: [ + { title: 'MINOR BREAKING CHANGES' }, + { title: 'MAJOR BREAKING CHANGES' } + ], + rawType: 'Feature' + } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'major' ); + } ); - it( 'returns "patch" if no breaking changes or features commits were made', () => { - const commits = [ - { isPublicCommit: true, notes: [], rawType: 'Fix' }, - { isPublicCommit: true, notes: [], rawType: 'Other' } - ]; - expect( getNewVersionType( commits ) ).to.equal( 'patch' ); - } ); + it( 'returns "patch" if no breaking changes or features commits were made', () => { + const commits = [ + { isPublicCommit: true, notes: [], rawType: 'Fix' }, + { isPublicCommit: true, notes: [], rawType: 'Other' } + ]; + expect( getNewVersionType( commits ) ).toEqual( 'patch' ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/getnpmtagfromversion.js b/packages/ckeditor5-dev-release-tools/tests/utils/getnpmtagfromversion.js index e04a71e10..1a457e85b 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/getnpmtagfromversion.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/getnpmtagfromversion.js @@ -3,38 +3,14 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect, vi } from 'vitest'; +import semver from 'semver'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +import getNpmTagFromVersion from '../../lib/utils/getnpmtagfromversion.js'; -describe( 'dev-release-tools/getNpmTagFromVersion', () => { - let stub, getNpmTagFromVersion; - - beforeEach( () => { - stub = { - semver: { - prerelease: sinon.stub() - } - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'semver', stub.semver ); - - getNpmTagFromVersion = require( '../../lib/utils/getnpmtagfromversion' ); - } ); - - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - } ); +vi.mock( 'semver' ); +describe( 'getNpmTagFromVersion()', () => { it( 'should return "latest" when processing a X.Y.Z version', () => { expect( getNpmTagFromVersion( '1.0.0' ) ).to.equal( 'latest' ); expect( getNpmTagFromVersion( '2.1.0' ) ).to.equal( 'latest' ); @@ -42,7 +18,7 @@ describe( 'dev-release-tools/getNpmTagFromVersion', () => { } ); it( 'should return "alpha" when processing a X.Y.Z-alpha.X version', () => { - stub.semver.prerelease.returns( [ 'alpha', 0 ] ); + vi.mocked( semver.prerelease ).mockReturnValue( [ 'alpha', 0 ] ); expect( getNpmTagFromVersion( '1.0.0-alpha.0' ) ).to.equal( 'alpha' ); expect( getNpmTagFromVersion( '2.1.0-alpha.0' ) ).to.equal( 'alpha' ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/getpackagespaths.js b/packages/ckeditor5-dev-release-tools/tests/utils/getpackagespaths.js index 51ff6ff5c..77f8a7a2c 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/getpackagespaths.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/getpackagespaths.js @@ -3,277 +3,265 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); - -describe( 'dev-release-tools/utils', () => { - describe( 'getPackagesPaths()', () => { - let getPackagesPaths, sandbox, getPackageJsonStub, getDirectoriesStub; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - getPackageJsonStub = sandbox.stub(); - getDirectoriesStub = sandbox.stub(); - - sandbox.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ); - - getPackagesPaths = proxyquire( '../../lib/utils/getpackagespaths', { - './getpackagejson': getPackageJsonStub, - '@ckeditor/ckeditor5-dev-utils': { - tools: { - getDirectories: getDirectoriesStub - } - } - } ); - } ); - - afterEach( () => { - sandbox.restore(); - } ); - - it( 'returns all found packages', () => { - getDirectoriesStub.returns( [ - 'ckeditor5-core', - 'ckeditor5-engine', - 'ckeditor5-utils' - ] ); - - const options = { - cwd: '/tmp', - packages: 'packages', - skipPackages: [], - skipMainRepository: true - }; - - getPackageJsonStub.onCall( 0 ).returns( { name: '@ckeditor/ckeditor5-core' } ); - getPackageJsonStub.onCall( 1 ).returns( { name: '@ckeditor/ckeditor5-engine' } ); - getPackageJsonStub.onCall( 2 ).returns( { name: '@ckeditor/ckeditor5-utils' } ); - - const pathsCollection = getPackagesPaths( options ); - - expect( pathsCollection.matched ).to.be.instanceof( Set ); - expect( pathsCollection.matched.size ).to.equal( 3 ); - expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-core' ) ).to.equal( true ); - expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-engine' ) ).to.equal( true ); - expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-utils' ) ).to.equal( true ); - - expect( pathsCollection.skipped ).to.be.instanceof( Set ); - expect( pathsCollection.skipped.size ).to.equal( 1 ); - expect( pathsCollection.skipped.has( '/tmp' ) ).to.equal( true ); - } ); - - it( 'allows ignoring specified packages (specified as array)', () => { - getDirectoriesStub.returns( [ - 'ckeditor5-core', - 'ckeditor5-engine', - 'ckeditor5-utils' - ] ); - - const options = { - cwd: '/tmp', - packages: 'packages', - skipPackages: [ - '@ckeditor/ckeditor5-utils' - ], - skipMainRepository: true - }; - - getPackageJsonStub.onCall( 0 ).returns( { name: '@ckeditor/ckeditor5-core' } ); - getPackageJsonStub.onCall( 1 ).returns( { name: '@ckeditor/ckeditor5-engine' } ); - getPackageJsonStub.onCall( 2 ).returns( { name: '@ckeditor/ckeditor5-utils' } ); - - const pathsCollection = getPackagesPaths( options ); - - expect( pathsCollection.matched ).to.be.instanceof( Set ); - expect( pathsCollection.matched.size ).to.equal( 2 ); - - expect( pathsCollection.skipped ).to.be.instanceof( Set ); - expect( pathsCollection.skipped.size ).to.equal( 2 ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp' ) ).to.equal( true ); - } ); - - it( 'allows ignoring specified packages (specified as string)', () => { - getDirectoriesStub.returns( [ - 'ckeditor5-core', - 'ckeditor5-engine', - 'ckeditor5-utils' - ] ); - - const options = { - cwd: '/tmp', - packages: 'packages', - skipPackages: '@ckeditor/ckeditor5-u*', - skipMainRepository: true - }; - getPackageJsonStub.onCall( 0 ).returns( { name: '@ckeditor/ckeditor5-core' } ); - getPackageJsonStub.onCall( 1 ).returns( { name: '@ckeditor/ckeditor5-engine' } ); - getPackageJsonStub.onCall( 2 ).returns( { name: '@ckeditor/ckeditor5-utils' } ); - - const pathsCollection = getPackagesPaths( options ); - - expect( pathsCollection.matched ).to.be.instanceof( Set ); - expect( pathsCollection.matched.size ).to.equal( 2 ); - - expect( pathsCollection.skipped ).to.be.instanceof( Set ); - expect( pathsCollection.skipped.size ).to.equal( 2 ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp' ) ).to.equal( true ); - } ); - - it( 'allows restricting the scope for packages', () => { - getDirectoriesStub.returns( [ - 'ckeditor5-core', - 'ckeditor5-engine', - 'ckeditor5-utils', - 'ckeditor5-build-classic', - 'ckeditor5-build-inline' - ] ); - - const options = { - cwd: '/tmp', - packages: 'packages', - scope: '@ckeditor/ckeditor5-build-*', - skipPackages: [], - skipMainRepository: true - }; - - getPackageJsonStub.onCall( 0 ).returns( { name: '@ckeditor/ckeditor5-core' } ); - getPackageJsonStub.onCall( 1 ).returns( { name: '@ckeditor/ckeditor5-engine' } ); - getPackageJsonStub.onCall( 2 ).returns( { name: '@ckeditor/ckeditor5-utils' } ); - getPackageJsonStub.onCall( 3 ).returns( { name: '@ckeditor/ckeditor5-build-classic' } ); - getPackageJsonStub.onCall( 4 ).returns( { name: '@ckeditor/ckeditor5-build-inline' } ); - - const pathsCollection = getPackagesPaths( options ); - - expect( pathsCollection.matched ).to.be.instanceof( Set ); - expect( pathsCollection.matched.size ).to.equal( 2 ); - expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-build-classic' ) ).to.equal( true ); - expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-build-inline' ) ).to.equal( true ); - - expect( pathsCollection.skipped ).to.be.instanceof( Set ); - expect( pathsCollection.skipped.size ).to.equal( 4 ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-core' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-engine' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp' ) ).to.equal( true ); - } ); - - it( 'allows restricting the scope for packages and works fine with "skipPackages" option', () => { - getDirectoriesStub.returns( [ - 'ckeditor5-core', - 'ckeditor5-engine', - 'ckeditor5-utils', - 'ckeditor5-build-classic', - 'ckeditor5-build-inline' - ] ); - - const options = { - cwd: '/tmp', - packages: 'packages', - scope: '@ckeditor/ckeditor5-build-*', - skipPackages: [ - '@ckeditor/ckeditor5-build-inline' - ], - skipMainRepository: true - }; - - getPackageJsonStub.onCall( 0 ).returns( { name: '@ckeditor/ckeditor5-core' } ); - getPackageJsonStub.onCall( 1 ).returns( { name: '@ckeditor/ckeditor5-engine' } ); - getPackageJsonStub.onCall( 2 ).returns( { name: '@ckeditor/ckeditor5-utils' } ); - getPackageJsonStub.onCall( 3 ).returns( { name: '@ckeditor/ckeditor5-build-classic' } ); - getPackageJsonStub.onCall( 4 ).returns( { name: '@ckeditor/ckeditor5-build-inline' } ); - - const pathsCollection = getPackagesPaths( options ); - - expect( pathsCollection.matched ).to.be.instanceof( Set ); - expect( pathsCollection.matched.size ).to.equal( 1 ); - expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-build-classic' ) ).to.equal( true ); - - expect( pathsCollection.skipped ).to.be.instanceof( Set ); - expect( pathsCollection.skipped.size ).to.equal( 5 ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-core' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-engine' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-build-inline' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp' ) ).to.equal( true ); - } ); - - it( 'allows returning the main repository', () => { - getDirectoriesStub.returns( [ - 'ckeditor5-core', - 'ckeditor5-engine', - 'ckeditor5-utils', - 'ckeditor5-build-classic', - 'ckeditor5-build-inline' - ] ); - - const options = { - cwd: '/tmp', - packages: 'packages', - skipPackages: [ - '@ckeditor/ckeditor5-*' - ], - skipMainRepository: false - }; - - getPackageJsonStub.onCall( 0 ).returns( { name: '@ckeditor/ckeditor5-core' } ); - getPackageJsonStub.onCall( 1 ).returns( { name: '@ckeditor/ckeditor5-engine' } ); - getPackageJsonStub.onCall( 2 ).returns( { name: '@ckeditor/ckeditor5-utils' } ); - getPackageJsonStub.onCall( 3 ).returns( { name: '@ckeditor/ckeditor5-build-classic' } ); - getPackageJsonStub.onCall( 4 ).returns( { name: '@ckeditor/ckeditor5-build-inline' } ); - - const pathsCollection = getPackagesPaths( options ); - - expect( pathsCollection.matched ).to.be.instanceof( Set ); - expect( pathsCollection.matched.size ).to.equal( 1 ); - expect( pathsCollection.matched.has( '/tmp' ) ).to.equal( true ); - - expect( pathsCollection.skipped ).to.be.instanceof( Set ); - expect( pathsCollection.skipped.size ).to.equal( 5 ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-core' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-engine' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-build-inline' ) ).to.equal( true ); - expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-build-classic' ) ).to.equal( true ); - } ); - - it( 'allows returning the main repository only (skipMainRepository=false)', () => { - const options = { - cwd: '/tmp', - packages: null - }; - - const pathsCollection = getPackagesPaths( options ); - - expect( pathsCollection.matched ).to.be.instanceof( Set ); - expect( pathsCollection.matched.size ).to.equal( 1 ); - expect( pathsCollection.matched.has( '/tmp' ) ).to.equal( true ); - - expect( pathsCollection.skipped ).to.be.instanceof( Set ); - expect( pathsCollection.skipped.size ).to.equal( 0 ); - } ); - - it( 'allows returning the main repository only (skipMainRepository=true)', () => { - const options = { - cwd: '/tmp', - packages: null, - skipMainRepository: true - }; - - const pathsCollection = getPackagesPaths( options ); - - expect( pathsCollection.matched ).to.be.instanceof( Set ); - expect( pathsCollection.matched.size ).to.equal( 0 ); - - expect( pathsCollection.skipped ).to.be.instanceof( Set ); - expect( pathsCollection.skipped.size ).to.equal( 1 ); - expect( pathsCollection.skipped.has( '/tmp' ) ).to.equal( true ); - } ); +import { describe, it, expect, vi } from 'vitest'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import getPackageJson from '../../lib/utils/getpackagejson.js'; +import getPackagesPaths from '../../lib/utils/getpackagespaths.js'; + +vi.mock( 'path', () => ( { + default: { + join: vi.fn( ( ...chunks ) => chunks.join( '/' ) ), + dirname: vi.fn() + } +} ) ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( '../../lib/utils/getpackagejson.js' ); + +describe( 'getPackagesPaths()', () => { + it( 'returns all found packages', () => { + vi.mocked( tools.getDirectories ).mockReturnValue( [ + 'ckeditor5-core', + 'ckeditor5-engine', + 'ckeditor5-utils' + ] ); + + vi.mocked( getPackageJson ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-core' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-engine' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-utils' } ); + + const options = { + cwd: '/tmp', + packages: 'packages', + skipPackages: [], + skipMainRepository: true + }; + + const pathsCollection = getPackagesPaths( options ); + + expect( pathsCollection.matched ).toBeInstanceOf( Set ); + expect( pathsCollection.matched.size ).toEqual( 3 ); + expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-core' ) ).toEqual( true ); + expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-engine' ) ).toEqual( true ); + expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-utils' ) ).toEqual( true ); + + expect( pathsCollection.skipped ).toBeInstanceOf( Set ); + expect( pathsCollection.skipped.size ).toEqual( 1 ); + expect( pathsCollection.skipped.has( '/tmp' ) ).toEqual( true ); + } ); + + it( 'allows ignoring specified packages (specified as array)', () => { + vi.mocked( tools.getDirectories ).mockReturnValue( [ + 'ckeditor5-core', + 'ckeditor5-engine', + 'ckeditor5-utils' + ] ); + + vi.mocked( getPackageJson ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-core' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-engine' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-utils' } ); + + const options = { + cwd: '/tmp', + packages: 'packages', + skipPackages: [ + '@ckeditor/ckeditor5-utils' + ], + skipMainRepository: true + }; + + const pathsCollection = getPackagesPaths( options ); + + expect( pathsCollection.matched ).toBeInstanceOf( Set ); + expect( pathsCollection.matched.size ).toEqual( 2 ); + + expect( pathsCollection.skipped ).toBeInstanceOf( Set ); + expect( pathsCollection.skipped.size ).toEqual( 2 ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp' ) ).toEqual( true ); + } ); + + it( 'allows ignoring specified packages (specified as string)', () => { + vi.mocked( tools.getDirectories ).mockReturnValue( [ + 'ckeditor5-core', + 'ckeditor5-engine', + 'ckeditor5-utils' + ] ); + + vi.mocked( getPackageJson ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-core' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-engine' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-utils' } ); + + const options = { + cwd: '/tmp', + packages: 'packages', + skipPackages: '@ckeditor/ckeditor5-u*', + skipMainRepository: true + }; + + const pathsCollection = getPackagesPaths( options ); + + expect( pathsCollection.matched ).toBeInstanceOf( Set ); + expect( pathsCollection.matched.size ).toEqual( 2 ); + + expect( pathsCollection.skipped ).toBeInstanceOf( Set ); + expect( pathsCollection.skipped.size ).toEqual( 2 ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp' ) ).toEqual( true ); + } ); + + it( 'allows restricting the scope for packages', () => { + vi.mocked( tools.getDirectories ).mockReturnValue( [ + 'ckeditor5-core', + 'ckeditor5-engine', + 'ckeditor5-utils', + 'ckeditor5-build-classic', + 'ckeditor5-build-inline' + ] ); + + vi.mocked( getPackageJson ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-core' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-engine' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-utils' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-build-classic' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-build-inline' } ); + + const options = { + cwd: '/tmp', + packages: 'packages', + scope: '@ckeditor/ckeditor5-build-*', + skipPackages: [], + skipMainRepository: true + }; + + const pathsCollection = getPackagesPaths( options ); + + expect( pathsCollection.matched ).toBeInstanceOf( Set ); + expect( pathsCollection.matched.size ).toEqual( 2 ); + expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-build-classic' ) ).toEqual( true ); + expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-build-inline' ) ).toEqual( true ); + + expect( pathsCollection.skipped ).toBeInstanceOf( Set ); + expect( pathsCollection.skipped.size ).toEqual( 4 ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-core' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-engine' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp' ) ).toEqual( true ); + } ); + + it( 'allows restricting the scope for packages and works fine with "skipPackages" option', () => { + vi.mocked( tools.getDirectories ).mockReturnValue( [ + 'ckeditor5-core', + 'ckeditor5-engine', + 'ckeditor5-utils', + 'ckeditor5-build-classic', + 'ckeditor5-build-inline' + ] ); + + vi.mocked( getPackageJson ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-core' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-engine' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-utils' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-build-classic' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-build-inline' } ); + + const options = { + cwd: '/tmp', + packages: 'packages', + scope: '@ckeditor/ckeditor5-build-*', + skipPackages: [ + '@ckeditor/ckeditor5-build-inline' + ], + skipMainRepository: true + }; + + const pathsCollection = getPackagesPaths( options ); + + expect( pathsCollection.matched ).toBeInstanceOf( Set ); + expect( pathsCollection.matched.size ).toEqual( 1 ); + expect( pathsCollection.matched.has( '/tmp/packages/ckeditor5-build-classic' ) ).toEqual( true ); + + expect( pathsCollection.skipped ).toBeInstanceOf( Set ); + expect( pathsCollection.skipped.size ).toEqual( 5 ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-core' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-engine' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-build-inline' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp' ) ).toEqual( true ); + } ); + + it( 'allows returning the main repository', () => { + vi.mocked( tools.getDirectories ).mockReturnValue( [ + 'ckeditor5-core', + 'ckeditor5-engine', + 'ckeditor5-utils', + 'ckeditor5-build-classic', + 'ckeditor5-build-inline' + ] ); + + vi.mocked( getPackageJson ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-core' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-engine' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-utils' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-build-classic' } ) + .mockReturnValueOnce( { name: '@ckeditor/ckeditor5-build-inline' } ); + + const options = { + cwd: '/tmp', + packages: 'packages', + skipPackages: [ + '@ckeditor/ckeditor5-*' + ], + skipMainRepository: false + }; + + const pathsCollection = getPackagesPaths( options ); + + expect( pathsCollection.matched ).toBeInstanceOf( Set ); + expect( pathsCollection.matched.size ).toEqual( 1 ); + expect( pathsCollection.matched.has( '/tmp' ) ).toEqual( true ); + + expect( pathsCollection.skipped ).toBeInstanceOf( Set ); + expect( pathsCollection.skipped.size ).toEqual( 5 ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-core' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-engine' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-utils' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-build-inline' ) ).toEqual( true ); + expect( pathsCollection.skipped.has( '/tmp/packages/ckeditor5-build-classic' ) ).toEqual( true ); + } ); + + it( 'allows returning the main repository only (skipMainRepository=false)', () => { + const options = { + cwd: '/tmp', + packages: null + }; + + const pathsCollection = getPackagesPaths( options ); + + expect( pathsCollection.matched ).toBeInstanceOf( Set ); + expect( pathsCollection.matched.size ).toEqual( 1 ); + expect( pathsCollection.matched.has( '/tmp' ) ).toEqual( true ); + + expect( pathsCollection.skipped ).toBeInstanceOf( Set ); + expect( pathsCollection.skipped.size ).toEqual( 0 ); + } ); + + it( 'allows returning the main repository only (skipMainRepository=true)', () => { + const options = { + cwd: '/tmp', + packages: null, + skipMainRepository: true + }; + + const pathsCollection = getPackagesPaths( options ); + + expect( pathsCollection.matched ).toBeInstanceOf( Set ); + expect( pathsCollection.matched.size ).toEqual( 0 ); + + expect( pathsCollection.skipped ).toBeInstanceOf( Set ); + expect( pathsCollection.skipped.size ).toEqual( 1 ); + expect( pathsCollection.skipped.has( '/tmp' ) ).toEqual( true ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/getwriteroptions.js b/packages/ckeditor5-dev-release-tools/tests/utils/getwriteroptions.js index 2dc30cb87..422c1605a 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/getwriteroptions.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/getwriteroptions.js @@ -3,105 +3,90 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect, vi } from 'vitest'; +import getWriterOptions from '../../lib/utils/getwriteroptions.js'; + +describe( 'getWriterOptions()', () => { + const transformSpy = vi.fn(); + + it( 'returns an object with writer options', () => { + const writerOptions = getWriterOptions( transformSpy ); + + expect( writerOptions ).to.have.property( 'transform', transformSpy ); + expect( writerOptions ).to.have.property( 'groupBy' ); + expect( writerOptions ).to.have.property( 'commitGroupsSort' ); + expect( writerOptions ).to.have.property( 'commitsSort' ); + expect( writerOptions ).to.have.property( 'noteGroupsSort' ); + expect( writerOptions ).to.have.property( 'mainTemplate' ); + expect( writerOptions ).to.have.property( 'headerPartial' ); + expect( writerOptions ).to.have.property( 'footerPartial' ); + + expect( writerOptions.commitsSort ).to.be.a( 'array' ); + expect( writerOptions.commitGroupsSort ).to.be.a( 'function' ); + expect( writerOptions.noteGroupsSort ).to.be.a( 'function' ); + } ); + + it( 'sorts notes properly', () => { + const writerOptions = getWriterOptions( transformSpy ); -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); + const noteGroups = [ + { title: 'BREAKING CHANGES', notes: [] }, + { title: 'MINOR BREAKING CHANGES', notes: [] }, + { title: 'MAJOR BREAKING CHANGES', notes: [] } + ]; + + expect( noteGroups.sort( writerOptions.noteGroupsSort ) ).to.deep.equal( [ + { title: 'MAJOR BREAKING CHANGES', notes: [] }, + { title: 'MINOR BREAKING CHANGES', notes: [] }, + { title: 'BREAKING CHANGES', notes: [] } + ] ); + } ); -describe( 'dev-release-tools/utils', () => { - let getWriterOptions, sandbox, transformSpy; + it( 'sorts notes properly (titles with emojis)', () => { + const writerOptions = getWriterOptions( transformSpy ); - beforeEach( () => { - transformSpy = sinon.spy(); - sandbox = sinon.createSandbox(); + const noteGroups = [ + { title: 'BREAKING CHANGES [ℹ](url)', notes: [] }, + { title: 'MINOR BREAKING CHANGES [ℹ](url)', notes: [] }, + { title: 'MAJOR BREAKING CHANGES [ℹ](url)', notes: [] } + ]; - getWriterOptions = require( '../../lib/utils/getwriteroptions' ); + expect( noteGroups.sort( writerOptions.noteGroupsSort ) ).to.deep.equal( [ + { title: 'MAJOR BREAKING CHANGES [ℹ](url)', notes: [] }, + { title: 'MINOR BREAKING CHANGES [ℹ](url)', notes: [] }, + { title: 'BREAKING CHANGES [ℹ](url)', notes: [] } + ] ); } ); - afterEach( () => { - sandbox.restore(); + it( 'sorts groups properly', () => { + const writerOptions = getWriterOptions( transformSpy ); + + const commitGroups = [ + { title: 'Other changes', commits: [] }, + { title: 'Features', commits: [] }, + { title: 'Bug fixes', commits: [] } + ]; + + expect( commitGroups.sort( writerOptions.commitGroupsSort ) ).to.deep.equal( [ + { title: 'Features', commits: [] }, + { title: 'Bug fixes', commits: [] }, + { title: 'Other changes', commits: [] } + ] ); } ); - describe( 'getWriterOptions()', () => { - it( 'returns an object with writer options', () => { - const writerOptions = getWriterOptions( transformSpy ); - - expect( writerOptions ).to.have.property( 'transform', transformSpy ); - expect( writerOptions ).to.have.property( 'groupBy' ); - expect( writerOptions ).to.have.property( 'commitGroupsSort' ); - expect( writerOptions ).to.have.property( 'commitsSort' ); - expect( writerOptions ).to.have.property( 'noteGroupsSort' ); - expect( writerOptions ).to.have.property( 'mainTemplate' ); - expect( writerOptions ).to.have.property( 'headerPartial' ); - expect( writerOptions ).to.have.property( 'footerPartial' ); - - expect( writerOptions.commitsSort ).to.be.a( 'array' ); - expect( writerOptions.commitGroupsSort ).to.be.a( 'function' ); - expect( writerOptions.noteGroupsSort ).to.be.a( 'function' ); - } ); - - it( 'sorts notes properly', () => { - const writerOptions = getWriterOptions( transformSpy ); - - const noteGroups = [ - { title: 'BREAKING CHANGES', notes: [] }, - { title: 'MINOR BREAKING CHANGES', notes: [] }, - { title: 'MAJOR BREAKING CHANGES', notes: [] } - ]; - - expect( noteGroups.sort( writerOptions.noteGroupsSort ) ).to.deep.equal( [ - { title: 'MAJOR BREAKING CHANGES', notes: [] }, - { title: 'MINOR BREAKING CHANGES', notes: [] }, - { title: 'BREAKING CHANGES', notes: [] } - ] ); - } ); - - it( 'sorts notes properly (titles with emojis)', () => { - const writerOptions = getWriterOptions( transformSpy ); - - const noteGroups = [ - { title: 'BREAKING CHANGES [ℹ](url)', notes: [] }, - { title: 'MINOR BREAKING CHANGES [ℹ](url)', notes: [] }, - { title: 'MAJOR BREAKING CHANGES [ℹ](url)', notes: [] } - ]; - - expect( noteGroups.sort( writerOptions.noteGroupsSort ) ).to.deep.equal( [ - { title: 'MAJOR BREAKING CHANGES [ℹ](url)', notes: [] }, - { title: 'MINOR BREAKING CHANGES [ℹ](url)', notes: [] }, - { title: 'BREAKING CHANGES [ℹ](url)', notes: [] } - ] ); - } ); - - it( 'sorts groups properly', () => { - const writerOptions = getWriterOptions( transformSpy ); - - const commitGroups = [ - { title: 'Other changes', commits: [] }, - { title: 'Features', commits: [] }, - { title: 'Bug fixes', commits: [] } - ]; - - expect( commitGroups.sort( writerOptions.commitGroupsSort ) ).to.deep.equal( [ - { title: 'Features', commits: [] }, - { title: 'Bug fixes', commits: [] }, - { title: 'Other changes', commits: [] } - ] ); - } ); - - it( 'sorts groups properly (titles with emojis)', () => { - const writerOptions = getWriterOptions( transformSpy ); - - const commitGroups = [ - { title: 'Other changes [ℹ](url)', commits: [] }, - { title: 'Features [ℹ](url)', commits: [] }, - { title: 'Bug fixes [ℹ](url)', commits: [] } - ]; - - expect( commitGroups.sort( writerOptions.commitGroupsSort ) ).to.deep.equal( [ - { title: 'Features [ℹ](url)', commits: [] }, - { title: 'Bug fixes [ℹ](url)', commits: [] }, - { title: 'Other changes [ℹ](url)', commits: [] } - ] ); - } ); + it( 'sorts groups properly (titles with emojis)', () => { + const writerOptions = getWriterOptions( transformSpy ); + + const commitGroups = [ + { title: 'Other changes [ℹ](url)', commits: [] }, + { title: 'Features [ℹ](url)', commits: [] }, + { title: 'Bug fixes [ℹ](url)', commits: [] } + ]; + + expect( commitGroups.sort( writerOptions.commitGroupsSort ) ).to.deep.equal( [ + { title: 'Features [ℹ](url)', commits: [] }, + { title: 'Bug fixes [ℹ](url)', commits: [] }, + { title: 'Other changes [ℹ](url)', commits: [] } + ] ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/isversionpublishablefortag.js b/packages/ckeditor5-dev-release-tools/tests/utils/isversionpublishablefortag.js index 37e025cc0..de70dc13e 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/isversionpublishablefortag.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/isversionpublishablefortag.js @@ -3,91 +3,67 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import semver from 'semver'; +import shellEscape from 'shell-escape'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +import isVersionPublishableForTag from '../../lib/utils/isversionpublishablefortag.js'; -describe( 'dev-release-tools/isVersionPublishableForTag', () => { - let stub, isVersionPublishableForTag; +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( 'semver' ); +vi.mock( 'shell-escape' ); +describe( 'isVersionPublishableForTag()', () => { beforeEach( () => { - stub = { - semver: { - lte: sinon.stub() - }, - devUtils: { - tools: { - shExec: sinon.stub() - } - }, - shellEscape: sinon.stub().callsFake( v => v[ 0 ] ) - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'semver', stub.semver ); - mockery.registerMock( 'shell-escape', stub.shellEscape ); - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', stub.devUtils ); - - isVersionPublishableForTag = require( '../../lib/utils/isversionpublishablefortag' ); - } ); - - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); + vi.mocked( shellEscape ).mockImplementation( v => v[ 0 ] ); } ); it( 'should return true if given version is available', async () => { - stub.semver.lte.returns( false ); - stub.devUtils.tools.shExec.resolves( '1.0.0\n' ); + vi.mocked( semver.lte ).mockReturnValue( false ); + vi.mocked( tools.shExec ).mockResolvedValue( '1.0.0\n' ); const result = await isVersionPublishableForTag( 'package-name', '1.0.1', 'latest' ); expect( result ).to.equal( true ); - expect( stub.semver.lte.callCount ).to.equal( 1 ); - expect( stub.semver.lte.firstCall.args ).to.deep.equal( [ '1.0.1', '1.0.0' ] ); - expect( stub.devUtils.tools.shExec.callCount ).to.equal( 1 ); - expect( stub.devUtils.tools.shExec.firstCall.firstArg ).to.equal( 'npm view package-name@latest version --silent' ); + expect( semver.lte ).toHaveBeenCalledTimes( 1 ); + expect( semver.lte ).toHaveBeenCalledWith( '1.0.1', '1.0.0' ); + expect( tools.shExec ).toHaveBeenCalledTimes( 1 ); + expect( tools.shExec ).toHaveBeenCalledWith( 'npm view package-name@latest version --silent', expect.anything() ); } ); it( 'should return false if given version is not available', async () => { - stub.semver.lte.returns( true ); - stub.devUtils.tools.shExec.resolves( '1.0.0\n' ); + vi.mocked( semver.lte ).mockReturnValue( true ); + vi.mocked( tools.shExec ).mockResolvedValue( '1.0.0\n' ); const result = await isVersionPublishableForTag( 'package-name', '1.0.0', 'latest' ); expect( result ).to.equal( false ); - expect( stub.semver.lte.callCount ).to.equal( 1 ); - expect( stub.semver.lte.firstCall.args ).to.deep.equal( [ '1.0.0', '1.0.0' ] ); - expect( stub.devUtils.tools.shExec.callCount ).to.equal( 1 ); - expect( stub.devUtils.tools.shExec.firstCall.firstArg ).to.equal( 'npm view package-name@latest version --silent' ); + expect( semver.lte ).toHaveBeenCalledTimes( 1 ); + expect( semver.lte ).toHaveBeenCalledWith( '1.0.0', '1.0.0' ); + expect( tools.shExec ).toHaveBeenCalledTimes( 1 ); + expect( tools.shExec ).toHaveBeenCalledWith( 'npm view package-name@latest version --silent', expect.anything() ); } ); it( 'should return true if given npm tag is not published yet', async () => { - stub.devUtils.tools.shExec.rejects( 'E404' ); + vi.mocked( tools.shExec ).mockRejectedValue( 'E404' ); const result = await isVersionPublishableForTag( 'package-name', '1.0.0', 'alpha' ); expect( result ).to.equal( true ); - expect( stub.semver.lte.callCount ).to.equal( 0 ); - expect( stub.devUtils.tools.shExec.callCount ).to.equal( 1 ); - expect( stub.devUtils.tools.shExec.firstCall.firstArg ).to.equal( 'npm view package-name@alpha version --silent' ); + expect( semver.lte ).not.toHaveBeenCalled(); + expect( tools.shExec ).toHaveBeenCalledTimes( 1 ); + expect( tools.shExec ).toHaveBeenCalledWith( 'npm view package-name@alpha version --silent', expect.anything() ); } ); it( 'should escape arguments passed to a shell command', async () => { - stub.semver.lte.returns( false ); - stub.devUtils.tools.shExec.resolves( '1.0.0\n' ); + vi.mocked( semver.lte ).mockReturnValue( false ); + vi.mocked( tools.shExec ).mockResolvedValue( '1.0.0\n' ); await isVersionPublishableForTag( 'package-name', '1.0.0', 'alpha' ); - expect( stub.shellEscape.callCount ).to.equal( 2 ); - expect( stub.shellEscape.firstCall.firstArg ).to.deep.equal( [ 'package-name' ] ); - expect( stub.shellEscape.secondCall.firstArg ).to.deep.equal( [ 'alpha' ] ); + expect( shellEscape ).toHaveBeenCalledTimes( 2 ); + expect( shellEscape ).toHaveBeenNthCalledWith( 1, [ 'package-name' ] ); + expect( shellEscape ).toHaveBeenNthCalledWith( 2, [ 'alpha' ] ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/parallelworker.js b/packages/ckeditor5-dev-release-tools/tests/utils/parallelworker.js new file mode 100644 index 000000000..75b0e42d1 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/parallelworker.js @@ -0,0 +1,48 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import { parentPort } from 'worker_threads'; +import virtual from 'virtual:parallelworker-integration-module'; + +const taskOptions = vi.hoisted( () => ( { + aNumber: 1, + bBoolean: false, + cString: 'foo' +} ) ); + +vi.mock( 'virtual:parallelworker-integration-module' ); +vi.mock( 'worker_threads', () => ( { + parentPort: { + postMessage: vi.fn() + }, + workerData: { + callbackModule: 'virtual:parallelworker-integration-module', + packages: [ + '/home/ckeditor/packages/a', + '/home/ckeditor/packages/b' + ], + taskOptions + } +} ) ); + +describe( 'parallelWorker (worker defined in executeInParallel())', () => { + it( 'should execute a module from specified path and pass a package path and task options as arguments', async () => { + await import( '../../lib/utils/parallelworker.js' ); + + // It's needed because `parallelworker` does not export anything. Instead, it processes + // an asynchronous loop. We must wait until the current JavaScript loop ends. Adding a new promise at the end + // forces it. + await new Promise( resolve => { + setTimeout( resolve, 100 ); + } ); + + expect( vi.mocked( parentPort ).postMessage ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( parentPort ).postMessage ).toHaveBeenCalledWith( 'done:package' ); + expect( vi.mocked( virtual ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( virtual ) ).toHaveBeenCalledWith( '/home/ckeditor/packages/a', taskOptions ); + expect( vi.mocked( virtual ) ).toHaveBeenCalledWith( '/home/ckeditor/packages/b', taskOptions ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/parseroptions.js b/packages/ckeditor5-dev-release-tools/tests/utils/parseroptions.js index f6844b789..8e522dbac 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/parseroptions.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/parseroptions.js @@ -3,20 +3,11 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect } from 'vitest'; +import parserOptions from '../../lib/utils/parseroptions.js'; -const expect = require( 'chai' ).expect; - -describe( 'dev-release-tools/utils', () => { - let parserOptions; - - beforeEach( () => { - parserOptions = require( '../../lib/utils/parseroptions' ); - } ); - - describe( 'parser-options', () => { - it( 'should not hoist closed tickets', () => { - expect( parserOptions.referenceActions ).to.deep.equal( [] ); - } ); +describe( 'parser-options', () => { + it( 'should not hoist closed tickets', () => { + expect( parserOptions.referenceActions ).to.deep.equal( [] ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/providenewversionformonorepository.js b/packages/ckeditor5-dev-release-tools/tests/utils/providenewversionformonorepository.js new file mode 100644 index 000000000..1de7c6fc5 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/providenewversionformonorepository.js @@ -0,0 +1,149 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import provideNewVersionForMonoRepository from '../../lib/utils/providenewversionformonorepository.js'; + +vi.mock( 'inquirer' ); +vi.mock( 'chalk', () => ( { + default: { + cyan: vi.fn( input => input ), + underline: vi.fn( input => input ) + } +} ) ); +vi.mock( '../../lib/utils/constants.js', () => ( { + CLI_INDENT_SIZE: 1 +} ) ); + +describe( 'provideNewVersionForMonoRepository()', () => { + beforeEach( () => { + vi.mocked( inquirer ).prompt.mockImplementation( input => { + const { default: version } = input[ 0 ]; + + return Promise.resolve( { version } ); + } ); + } ); + + it( 'bumps major version', async () => { + await expect( provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo', 'major' ) ) + .resolves.toEqual( '2.0.0' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version (current highest: "1.0.0" found in "@ckeditor/foo", suggested: "2.0.0"):', + default: '2.0.0' + } ) + ] ) + ); + expect( vi.mocked( chalk ).underline ).toHaveBeenCalledExactlyOnceWith( '@ckeditor/foo' ); + } ); + + it( 'bumps minor version', async () => { + await expect( provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo', 'minor' ) ) + .resolves.toEqual( '1.1.0' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version (current highest: "1.0.0" found in "@ckeditor/foo", suggested: "1.1.0"):', + default: '1.1.0' + } ) + ] ) + ); + expect( vi.mocked( chalk ).underline ).toHaveBeenCalledExactlyOnceWith( '@ckeditor/foo' ); + } ); + + it( 'bumps patch version', async () => { + await expect( provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo', 'patch' ) ) + .resolves.toEqual( '1.0.1' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version (current highest: "1.0.0" found in "@ckeditor/foo", suggested: "1.0.1"):', + default: '1.0.1' + } ) + ] ) + ); + expect( vi.mocked( chalk ).underline ).toHaveBeenCalledExactlyOnceWith( '@ckeditor/foo' ); + } ); + + it( 'allows attaching the helper as a part of another process (indent=0)', async () => { + await expect( provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo', 'major', { indentLevel: 0 } ) ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + prefix: '?' + } ) + ] ) + ); + + expect( vi.mocked( chalk ).cyan ).toHaveBeenCalledExactlyOnceWith( '?' ); + } ); + + it( 'allows attaching the helper as a part of another process (indent=3)', async () => { + await expect( provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo', 'major', { indentLevel: 3 } ) ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + // CLI indent is mocked as _a space per indent size_. + prefix: ' ?' + } ) + ] ) + ); + + expect( vi.mocked( chalk ).cyan ).toHaveBeenCalledExactlyOnceWith( '?' ); + } ); + + it( 'removes spaces from provided version', async () => { + await provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + filter: expect.any( Function ) + } ) + ] ) + ); + + const [ firstCall ] = vi.mocked( inquirer ).prompt.mock.calls; + const [ firstArgument ] = firstCall; + const [ firstQuestion ] = firstArgument; + const { filter } = firstQuestion; + + expect( filter( ' 0.0.1' ) ).to.equal( '0.0.1' ); + expect( filter( '0.0.1 ' ) ).to.equal( '0.0.1' ); + expect( filter( ' 0.0.1 ' ) ).to.equal( '0.0.1' ); + } ); + + it( 'validates the provided version', async () => { + await provideNewVersionForMonoRepository( '1.0.0', '@ckeditor/foo' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + validate: expect.any( Function ) + } ) + ] ) + ); + + const [ firstCall ] = vi.mocked( inquirer ).prompt.mock.calls; + const [ firstArgument ] = firstCall; + const [ firstQuestion ] = firstArgument; + const { validate } = firstQuestion; + + expect( validate( '2.0.0' ) ).to.equal( true ); + expect( validate( '1.1.0' ) ).to.equal( true ); + expect( validate( '1.0.0' ) ).to.equal( 'Provided version must be higher than "1.0.0".' ); + expect( validate( 'skip' ) ).to.equal( 'Please provide a valid version.' ); + expect( validate( 'internal' ) ).to.equal( 'Please provide a valid version.' ); + expect( validate( '0.1' ) ).to.equal( 'Please provide a valid version.' ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/providetoken.js b/packages/ckeditor5-dev-release-tools/tests/utils/providetoken.js new file mode 100644 index 000000000..745a6ced5 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/providetoken.js @@ -0,0 +1,50 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import inquirer from 'inquirer'; +import provideToken from '../../lib/utils/providetoken.js'; + +vi.mock( 'inquirer' ); + +describe( 'provideToken()', () => { + beforeEach( () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { token: 'MyPassword' } ); + } ); + + it( 'user is able to provide the token', async () => { + await expect( provideToken() ).resolves.toEqual( 'MyPassword' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'token', + type: 'password', + message: 'Provide the GitHub token:' + } ) + ] ) + ); + } ); + + it( 'token must contain 40 characters', async () => { + await provideToken(); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + validate: expect.any( Function ) + } ) + ] ) + ); + + const [ firstCall ] = vi.mocked( inquirer ).prompt.mock.calls; + const [ firstArgument ] = firstCall; + const [ firstQuestion ] = firstArgument; + const { validate } = firstQuestion; + + expect( validate( 'abc' ) ).to.equal( 'Please provide a valid token.' ); + expect( validate( 'a'.repeat( 40 ) ) ).to.equal( true ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/provideversion.js b/packages/ckeditor5-dev-release-tools/tests/utils/provideversion.js new file mode 100644 index 000000000..1ba6c2704 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/provideversion.js @@ -0,0 +1,203 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import inquirer from 'inquirer'; +import provideVersion from '../../lib/utils/provideversion.js'; + +vi.mock( 'inquirer' ); + +describe( 'provideVersion()', () => { + beforeEach( () => { + vi.mocked( inquirer ).prompt.mockImplementation( input => { + const { default: version } = input[ 0 ]; + + return Promise.resolve( { version } ); + } ); + } ); + + it( 'should use a specified version if is matches the semver standard', async () => { + await expect( provideVersion( '1.0.0', '1.1.0' ) ).resolves.toEqual( '1.1.0' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version, "skip" or "internal" (suggested: "1.1.0", current: "1.0.0"):', + default: '1.1.0' + } ) + ] ) + ); + } ); + + it( 'should suggest proper "major" version for public package', async () => { + await expect( provideVersion( '1.0.0', 'major' ) ).resolves.toEqual( '2.0.0' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version, "skip" or "internal" (suggested: "2.0.0", current: "1.0.0"):', + default: '2.0.0' + } ) + ] ) + ); + } ); + + it( 'should suggest proper "minor" version for public package', async () => { + await expect( provideVersion( '1.0.0', 'minor' ) ).resolves.toEqual( '1.1.0' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version, "skip" or "internal" (suggested: "1.1.0", current: "1.0.0"):', + default: '1.1.0' + } ) + ] ) + ); + } ); + + it( 'should suggest proper "patch" version for public package', async () => { + await expect( provideVersion( '1.0.0', 'patch' ) ).resolves.toEqual( '1.0.1' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version, "skip" or "internal" (suggested: "1.0.1", current: "1.0.0"):', + default: '1.0.1' + } ) + ] ) + ); + } ); + + it( 'should suggest "skip" version for package which does not contain changes (proposed null)', async () => { + await expect( provideVersion( '1.0.0', null ) ).resolves.toEqual( 'skip' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version, "skip" or "internal" (suggested: "skip", current: "1.0.0"):', + default: 'skip' + } ) + ] ) + ); + } ); + + it( 'should suggest "skip" version for package which does not contain changes (proposed "skip")', async () => { + await expect( provideVersion( '1.0.0', 'skip' ) ).resolves.toEqual( 'skip' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version, "skip" or "internal" (suggested: "skip", current: "1.0.0"):', + default: 'skip' + } ) + ] ) + ); + } ); + + it( 'should suggest "minor" instead of "major" version for non-public package', async () => { + await expect( provideVersion( '0.7.0', 'minor' ) ).resolves.toEqual( '0.8.0' ); + } ); + + it( 'should suggest proper "patch" version for non-public package', async () => { + await expect( provideVersion( '0.7.0', 'patch' ) ).resolves.toEqual( '0.7.1' ); + } ); + + it( 'returns "internal" if suggested version was "internal"', async () => { + await expect( provideVersion( '0.1.0', 'internal' ) ).resolves.toEqual( 'internal' ); + } ); + + it( 'allows disabling "internal" version', async () => { + await expect( provideVersion( '0.1.0', 'major', { disableInternalVersion: true } ) ) + .resolves.toEqual( expect.any( String ) ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + message: 'Type the new version or "skip" (suggested: "0.2.0", current: "0.1.0"):', + default: expect.any( String ) + } ) + ] ) + ); + } ); + + it( 'returns "skip" if suggested version was "internal" but it is disabled', async () => { + await expect( provideVersion( '0.1.0', 'internal', { disableInternalVersion: true } ) ) + .resolves.toEqual( 'skip' ); + } ); + + it( 'should suggest proper pre-release version for pre-release package (major bump)', async () => { + await expect( provideVersion( '1.0.0-alpha.1', 'major' ) ).resolves.toEqual( '1.0.0-alpha.2' ); + } ); + + it( 'should suggest proper pre-release version for pre-release package (minor bump)', async () => { + await expect( provideVersion( '1.0.0-alpha.1', 'minor' ) ).resolves.toEqual( '1.0.0-alpha.2' ); + } ); + + it( 'should suggest proper pre-release version for pre-release package (patch bump)', async () => { + await expect( provideVersion( '1.0.0-alpha.1', 'patch' ) ).resolves.toEqual( '1.0.0-alpha.2' ); + } ); + + it( 'removes spaces from provided version', async () => { + await provideVersion( '1.0.0', 'major' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + filter: expect.any( Function ) + } ) + ] ) + ); + const [ firstCall ] = vi.mocked( inquirer ).prompt.mock.calls; + const [ firstArgument ] = firstCall; + const [ firstQuestion ] = firstArgument; + const { filter } = firstQuestion; + + expect( filter( ' 0.0.1' ) ).to.equal( '0.0.1' ); + expect( filter( '0.0.1 ' ) ).to.equal( '0.0.1' ); + expect( filter( ' 0.0.1 ' ) ).to.equal( '0.0.1' ); + } ); + + it( 'validates the provided version (disableInternalVersion=false)', async () => { + await provideVersion( '1.0.0', 'major' ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + validate: expect.any( Function ) + } ) + ] ) + ); + const [ firstCall ] = vi.mocked( inquirer ).prompt.mock.calls; + const [ firstArgument ] = firstCall; + const [ firstQuestion ] = firstArgument; + const { validate } = firstQuestion; + + expect( validate( 'skip' ) ).to.equal( true ); + expect( validate( 'internal' ) ).to.equal( true ); + expect( validate( '2.0.0' ) ).to.equal( true ); + expect( validate( '0.1' ) ).to.equal( 'Please provide a valid version.' ); + } ); + + it( 'validates the provided version (disableInternalVersion=true)', async () => { + await provideVersion( '1.0.0', 'major', { disableInternalVersion: true } ); + + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( + expect.arrayContaining( [ + expect.objectContaining( { + validate: expect.any( Function ) + } ) + ] ) + ); + const [ firstCall ] = vi.mocked( inquirer ).prompt.mock.calls; + const [ firstArgument ] = firstCall; + const [ firstQuestion ] = firstArgument; + const { validate } = firstQuestion; + + expect( validate( 'skip' ) ).to.equal( true ); + expect( validate( 'internal' ) ).to.equal( 'Please provide a valid version.' ); + expect( validate( '2.0.0' ) ).to.equal( true ); + expect( validate( '0.1' ) ).to.equal( 'Please provide a valid version.' ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/publishpackageonnpmcallback.js b/packages/ckeditor5-dev-release-tools/tests/utils/publishpackageonnpmcallback.js index 88ed0fcbe..aa49a1a50 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/publishpackageonnpmcallback.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/publishpackageonnpmcallback.js @@ -3,130 +3,115 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import fs from 'fs-extra'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import publishPackageOnNpmCallback from '../../lib/utils/publishpackageonnpmcallback.js'; + +vi.mock( 'fs-extra' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); + +describe( 'publishPackageOnNpmCallback()', () => { + beforeEach( () => { + vi.mocked( tools.shExec ).mockResolvedValue(); + vi.mocked( fs.remove ).mockResolvedValue(); + } ); -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); + it( 'should publish package on npm with provided npm tag', () => { + const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; + + return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) + .then( () => { + expect( tools.shExec ).toHaveBeenCalledTimes( 1 ); + expect( tools.shExec ).toHaveBeenCalledWith( + 'npm publish --access=public --tag nightly', + expect.objectContaining( { + cwd: packagePath + } ) + ); + } ); + } ); -describe( 'dev-release-tools/utils', () => { - describe( 'publishPackageOnNpmCallback()', () => { - let publishPackageOnNpmCallback, sandbox, stubs; + it( 'should publish packages on npm asynchronously', () => { + const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; + + return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) + .then( () => { + expect( tools.shExec ).toHaveBeenCalledTimes( 1 ); + expect( tools.shExec ).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining( { + async: true + } ) + ); + } ); + } ); - beforeEach( () => { - sandbox = sinon.createSandbox(); + it( 'should set the verbosity level to "error" during publishing packages', () => { + const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; + + return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) + .then( () => { + expect( tools.shExec ).toHaveBeenCalledTimes( 1 ); + expect( tools.shExec ).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining( { + verbosity: 'error' + } ) + ); + } ); + } ); - stubs = { - fs: { - remove: sandbox.stub().resolves() - }, - devUtils: { - tools: { - shExec: sandbox.stub().resolves() - } - } - }; + it( 'should remove package directory after publishing on npm', () => { + const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false + return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) + .then( () => { + expect( fs.remove ).toHaveBeenCalledTimes( 1 ); + expect( fs.remove ).toHaveBeenCalledWith( packagePath ); } ); + } ); - mockery.registerMock( 'fs-extra', stubs.fs ); - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', stubs.devUtils ); - - publishPackageOnNpmCallback = require( '../../lib/utils/publishpackageonnpmcallback' ); - } ); - - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); - } ); - - it( 'should publish package on npm with provided npm tag', () => { - const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; - - return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) - .then( () => { - expect( stubs.devUtils.tools.shExec.callCount ).to.equal( 1 ); - expect( stubs.devUtils.tools.shExec.firstCall.args[ 0 ] ).to.equal( 'npm publish --access=public --tag nightly' ); - expect( stubs.devUtils.tools.shExec.firstCall.args[ 1 ] ).to.have.property( 'cwd', packagePath ); - } ); - } ); - - it( 'should publish packages on npm asynchronously', () => { - const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; - - return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) - .then( () => { - expect( stubs.devUtils.tools.shExec.callCount ).to.equal( 1 ); - expect( stubs.devUtils.tools.shExec.firstCall.args[ 1 ] ).to.have.property( 'async', true ); - } ); - } ); - - it( 'should set the verbosity level to "error" during publishing packages', () => { - const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; - - return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) - .then( () => { - expect( stubs.devUtils.tools.shExec.callCount ).to.equal( 1 ); - expect( stubs.devUtils.tools.shExec.firstCall.args[ 1 ] ).to.have.property( 'verbosity', 'error' ); - } ); - } ); - - it( 'should remove package directory after publishing on npm', () => { - const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; - - return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) - .then( () => { - expect( stubs.fs.remove.callCount ).to.equal( 1 ); - expect( stubs.fs.remove.firstCall.args[ 0 ] ).to.equal( packagePath ); - } ); - } ); - - it( 'should throw when publishing on npm failed', () => { - stubs.devUtils.tools.shExec.rejects(); - - const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; - - return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error ).to.be.an( 'Error' ); - expect( error.message ).to.equal( 'Unable to publish "ckeditor5-foo" package.' ); - } - ); - } ); + it( 'should throw when publishing on npm failed', () => { + vi.mocked( tools.shExec ).mockRejectedValue( new Error( 'Unexpected error.' ) ); - it( 'should not remove a package directory when publishing on npm failed', () => { - stubs.devUtils.tools.shExec.rejects(); + const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; - const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; + return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) + .then( + () => { + throw new Error( 'Expected to be rejected.' ); + }, + error => { + expect( error ).toBeInstanceOf( Error ); + expect( error.message ).toEqual( 'Unable to publish "ckeditor5-foo" package.' ); + } + ); + } ); - return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - () => { - expect( stubs.fs.remove.callCount ).to.equal( 0 ); - } - ); - } ); + it( 'should not remove a package directory when publishing on npm failed', () => { + vi.mocked( tools.shExec ).mockRejectedValue( new Error( 'Unexpected error.' ) ); + + const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; + + return publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ) + .then( + () => { + throw new Error( 'Expected to be rejected.' ); + }, + () => { + expect( fs.remove ).not.toHaveBeenCalled(); + } + ); + } ); - it( 'should not remove a package directory and not throw error when publishing on npm failed with code 409', async () => { - stubs.devUtils.tools.shExec.rejects( new Error( 'code E409' ) ); + it( 'should not remove a package directory and not throw error when publishing on npm failed with code 409', async () => { + vi.mocked( tools.shExec ).mockRejectedValue( new Error( 'code E409' ) ); - const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; + const packagePath = '/workspace/ckeditor5/packages/ckeditor5-foo'; - await publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ); + await publishPackageOnNpmCallback( packagePath, { npmTag: 'nightly' } ); - expect( stubs.fs.remove.callCount ).to.equal( 0 ); - } ); + expect( fs.remove ).not.toHaveBeenCalled(); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/savechangelog.js b/packages/ckeditor5-dev-release-tools/tests/utils/savechangelog.js new file mode 100644 index 000000000..a28d59f9c --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/savechangelog.js @@ -0,0 +1,45 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import fs from 'fs'; +import path from 'path'; +import { describe, expect, it, vi } from 'vitest'; +import saveChangelog from '../../lib/utils/savechangelog.js'; + +vi.mock( 'fs' ); +vi.mock( 'path', () => ( { + default: { + join: vi.fn( ( ...chunks ) => chunks.join( '/' ) ) + } +} ) ); +vi.mock( '../../lib/utils/constants.js', () => ( { + CHANGELOG_FILE: 'changelog.md' +} ) ); + +describe( 'saveChangelog()', () => { + it( 'saves the changelog (default cwd)', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/tmp' ); + + saveChangelog( 'New content.' ); + + expect( vi.mocked( path ).join ).toHaveBeenCalledExactlyOnceWith( '/tmp', 'changelog.md' ); + expect( vi.mocked( fs ).writeFileSync ).toHaveBeenCalledExactlyOnceWith( + '/tmp/changelog.md', + 'New content.', + 'utf-8' + ); + } ); + + it( 'saves the changelog (allows passing a custom cwd)', () => { + saveChangelog( 'New content.', '/custom/cwd' ); + + expect( vi.mocked( path ).join ).toHaveBeenCalledExactlyOnceWith( '/custom/cwd', 'changelog.md' ); + expect( vi.mocked( fs ).writeFileSync ).toHaveBeenCalledExactlyOnceWith( + '/custom/cwd/changelog.md', + 'New content.', + 'utf-8' + ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/transformcommitfactory.js b/packages/ckeditor5-dev-release-tools/tests/utils/transformcommitfactory.js index 2df92a0f3..ef0b185bc 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/transformcommitfactory.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/transformcommitfactory.js @@ -3,175 +3,546 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import getChangedFilesForCommit from '../../lib/utils/getchangedfilesforcommit.js'; +import transformCommitFactory from '../../lib/utils/transformcommitfactory.js'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +vi.mock( '../../lib/utils/getchangedfilesforcommit.js' ); -describe( 'dev-release-tools/utils', () => { - describe( 'transformCommitFactory()', () => { - let transformCommitFactory, sandbox, stubs; +describe( 'transformCommitFactory()', () => { + it( 'returns a function', () => { + expect( transformCommitFactory() ).to.be.a( 'function' ); + } ); + + describe( 'options.treatMajorAsMinorBreakingChange = true', () => { + it( 'treats "MAJOR BREAKING CHANGES" as "MINOR BREAKING CHANGES"', () => { + const transformCommit = transformCommitFactory( { + treatMajorAsMinorBreakingChange: true, + useExplicitBreakingChangeGroups: true + } ); + + const rawCommit = { + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix.', + type: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes: [ + { title: 'BREAKING CHANGE', text: 'Note 1.' }, + { title: 'MAJOR BREAKING CHANGES', text: 'Note 2.' } + ] + }; + + const commit = transformCommit( rawCommit ); + + expect( commit.notes[ 0 ].title ).to.equal( 'MINOR BREAKING CHANGES' ); + expect( commit.notes[ 1 ].title ).to.equal( 'MINOR BREAKING CHANGES' ); + } ); + } ); + + describe( 'transformCommit()', () => { + let transformCommit; beforeEach( () => { - sandbox = sinon.createSandbox(); + transformCommit = transformCommitFactory(); + } ); + + it( 'returns a new instance of object instead od modifying passed one', () => { + const notes = [ + { title: 'BREAKING CHANGES', text: 'Foo-Text', scope: null }, + { title: 'BREAKING CHANGES', text: 'Bar-Text', scope: null } + ]; + + const rawCommit = { + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix.', + type: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes + }; + + const commit = transformCommit( rawCommit ); + + // Notes cannot be the same but they should be equal. + expect( commit.notes ).to.not.equal( rawCommit.notes ); + expect( commit.notes ).to.deep.equal( rawCommit.notes ); + } ); - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false + it( 'returns files that were changed with the commit', () => { + const rawCommit = { + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix.', + type: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes: [] + }; + + const files = [ + 'a/b/y.txt', + 'c/d/z.md' + ]; + + vi.mocked( getChangedFilesForCommit ).mockReturnValue( files ); + + const commit = transformCommit( rawCommit ); + + expect( getChangedFilesForCommit ).toHaveBeenCalledTimes( 1 ); + expect( commit.files ).to.deep.equal( files ); + } ); + + it( 'returns non-public commit', () => { + const rawCommit = { + hash: '684997d', + header: 'Docs: README.', + type: 'Docs', + subject: 'README.', + body: null, + footer: null, + notes: [] + }; + + const newCommit = transformCommit( rawCommit ); + + expect( newCommit ).to.not.equal( undefined ); + } ); + + it( 'groups "BREAKING CHANGES" and "BREAKING CHANGE" as "MAJOR BREAKING CHANGES"', () => { + const rawCommit = { + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix.', + type: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes: [ + { title: 'BREAKING CHANGE', text: 'Note 1.' }, + { title: 'BREAKING CHANGES', text: 'Note 2.' } + ] + }; + + const transformCommit = transformCommitFactory( { + useExplicitBreakingChangeGroups: true } ); + const commit = transformCommit( rawCommit ); - stubs = { - getPackageJson: () => { - return { - repository: 'https://github.com/ckeditor/ckeditor5-dev' - }; - }, - getChangedFilesForCommit: sandbox.stub() + expect( commit.notes[ 0 ].title ).to.equal( 'MAJOR BREAKING CHANGES' ); + expect( commit.notes[ 1 ].title ).to.equal( 'MAJOR BREAKING CHANGES' ); + } ); + + it( 'makes proper links in the commit subject', () => { + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Fix: Simple fix. See ckeditor/ckeditor5#1. Thanks to @CKEditor. Closes #2.', + type: 'Fix', + subject: 'Simple fix. See ckeditor/ckeditor5#1. Thanks to @CKEditor. Closes #2.', + body: null, + footer: null, + notes: [] }; - mockery.registerMock( './getpackagejson', stubs.getPackageJson ); - mockery.registerMock( './getchangedfilesforcommit', stubs.getChangedFilesForCommit ); + const commit = transformCommit( rawCommit ); + + const expectedSubject = 'Simple fix. ' + + 'See [ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1). ' + + 'Thanks to [@CKEditor](https://github.com/CKEditor). ' + + 'Closes [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2).'; - transformCommitFactory = require( '../../lib/utils/transformcommitfactory' ); + expect( commit.subject ).to.equal( expectedSubject ); } ); - afterEach( () => { - sandbox.restore(); - mockery.disable(); + it( 'makes proper links in the commit body', () => { + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Fix: Simple fix. Closes #2.', + type: 'Fix', + subject: 'Simple fix. Closes #2', + body: 'See ckeditor/ckeditor5#1. Thanks to @CKEditor. Read more #2.', + footer: null, + notes: [] + }; + + const commit = transformCommit( rawCommit ); + + // Remember about the indent in commit body. + const expectedBody = ' See [ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1). ' + + 'Thanks to [@CKEditor](https://github.com/CKEditor). ' + + 'Read more [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2).'; + + expect( commit.body ).to.equal( expectedBody ); } ); - it( 'returns a function', () => { - expect( transformCommitFactory() ).to.be.a( 'function' ); + it( 'makes proper links in the commit notes', () => { + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Fix: Simple fix. Closes #2.', + type: 'Fix', + subject: 'Simple fix. Closes #2', + body: null, + footer: null, + notes: [ + { + title: 'BREAKING CHANGES', + text: 'See ckeditor/ckeditor5#1. Thanks to @CKEditor.' + }, + { + title: 'BREAKING CHANGES', + text: 'Read more #2.' + } + ] + }; + + const commit = transformCommit( rawCommit ); + + const expectedFirstNoteText = 'See [ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1). ' + + 'Thanks to [@CKEditor](https://github.com/CKEditor).'; + + const expectedSecondNodeText = 'Read more [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2).'; + + expect( commit.notes[ 0 ].text ).to.equal( expectedFirstNoteText ); + expect( commit.notes[ 1 ].text ).to.equal( expectedSecondNodeText ); } ); - describe( 'options.treatMajorAsMinorBreakingChange = true', () => { - it( 'treats "MAJOR BREAKING CHANGES" as "MINOR BREAKING CHANGES"', () => { - const transformCommit = transformCommitFactory( { - treatMajorAsMinorBreakingChange: true, - useExplicitBreakingChangeGroups: true - } ); + it( 'attaches additional commit description with correct indent', () => { + const commitDescription = [ + '* Release task - rebuilt module for collecting dependencies to release.', + '* Used `semver` package for bumping the version (instead of a custom module).' + ]; + + const commitDescriptionWithIndents = [ + ' * Release task - rebuilt module for collecting dependencies to release.', + ' * Used `semver` package for bumping the version (instead of a custom module).' + ].join( '\n' ); + + const rawCommit = { + header: 'Feature: Introduced a brand new release tools with a new set of requirements. See #64.', + hash: 'dea35014ab610be0c2150343c6a8a68620cfe5ad', + body: commitDescription.join( '\n' ), + footer: null, + mentions: [], + type: 'Feature', + subject: 'Introduced a brand new release tools with a new set of requirements. See #64.', + notes: [] + }; - const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix.', - type: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [ - { title: 'BREAKING CHANGE', text: 'Note 1.' }, - { title: 'MAJOR BREAKING CHANGES', text: 'Note 2.' } - ] - }; + const commit = transformCommit( rawCommit ); - const commit = transformCommit( rawCommit ); + expect( commit.type ).to.equal( 'Features' ); + expect( commit.subject ).to.equal( 'Introduced a brand new release tools with a new set of requirements. ' + + 'See [#64](https://github.com/ckeditor/ckeditor5-dev/issues/64).' ); + expect( commit.body ).to.equal( commitDescriptionWithIndents ); + } ); - expect( commit.notes[ 0 ].title ).to.equal( 'MINOR BREAKING CHANGES' ); - expect( commit.notes[ 1 ].title ).to.equal( 'MINOR BREAKING CHANGES' ); - } ); + it( 'removes references to issues', () => { + const rawCommit = { + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix.', + type: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes: [], + references: [ + { issue: '11' }, + { issue: '12' } + ] + }; + + const commit = transformCommit( rawCommit ); + + expect( commit.references ).to.equal( undefined ); + } ); + + it( 'uses commit\'s footer as a commit\'s body when commit does not have additional notes', () => { + const rawCommit = { + hash: 'dea35014ab610be0c2150343c6a8a68620cfe5ad', + header: 'Feature: Introduced a brand new release tools with a new set of requirements.', + type: 'Feature', + subject: 'Introduced a brand new release tools with a new set of requirements.', + body: null, + footer: 'Additional description has been parsed as a footer but it should be a body.', + notes: [] + }; + + const commit = transformCommit( rawCommit ); + + expect( commit.body ).to.equal( + ' Additional description has been parsed as a footer but it should be a body.' + ); + expect( commit.footer ).to.equal( null ); } ); - describe( 'transformCommit()', () => { - let transformCommit; + it( 'removes [skip ci] from the commit message', () => { + const rawCommit = { + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: README. [skip ci]', + type: 'Fix', + subject: 'README. [skip ci]', + body: null, + footer: null, + notes: [] + }; - beforeEach( () => { - transformCommit = transformCommitFactory(); - } ); + const commit = transformCommit( rawCommit ); + + expect( commit.subject ).to.equal( 'README.' ); + } ); + + it( 'includes "repositoryUrl" where the commit has been done', () => { + const notes = [ + { title: 'Foo', text: 'Foo-Text' }, + { title: 'Bar', text: 'Bar-Text' } + ]; + + const rawCommit = { + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix.', + type: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes + }; + + const commit = transformCommit( rawCommit ); + + expect( commit.repositoryUrl ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + } ); + + it( 'treats all "* BREAKING CHANGES" notes as "BREAKING CHANGE"', () => { + const rawCommit = { + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix.', + type: 'Fix', + subject: 'Simple fix.', + body: null, + footer: null, + notes: [ + { title: 'BREAKING CHANGE', text: 'Note 1.' }, + { title: 'BREAKING CHANGES', text: 'Note 2.' }, + { title: 'MAJOR BREAKING CHANGE', text: 'Note 3.' }, + { title: 'MAJOR BREAKING CHANGES', text: 'Note 4.' }, + { title: 'MINOR BREAKING CHANGE', text: 'Note 5.' }, + { title: 'MINOR BREAKING CHANGES', text: 'Note 6.' } + ] + }; + + const commit = transformCommit( rawCommit ); + + expect( commit.notes[ 0 ].title ).to.equal( 'BREAKING CHANGES' ); + expect( commit.notes[ 1 ].title ).to.equal( 'BREAKING CHANGES' ); + expect( commit.notes[ 2 ].title ).to.equal( 'BREAKING CHANGES' ); + expect( commit.notes[ 3 ].title ).to.equal( 'BREAKING CHANGES' ); + expect( commit.notes[ 4 ].title ).to.equal( 'BREAKING CHANGES' ); + expect( commit.notes[ 5 ].title ).to.equal( 'BREAKING CHANGES' ); + } ); + + it( 'removes duplicated notes from the footer', () => { + const notes = [ + { title: 'BREAKING CHANGES', text: 'Foo.', scope: null }, + { title: 'BREAKING CHANGES', text: 'Bar-Text.', scope: null } + ]; + + const rawCommit = { + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix.', + type: 'Fix', + subject: 'Simple fix.', + body: null, + footer: [ + 'BREAKING CHANGES: Foo.', + 'NOTE: Do not remove me.', + 'BREAKING CHANGES: Bar-Text.' + ].join( '\n' ), + notes + }; + + const commit = transformCommit( rawCommit ); + + expect( commit.body ).to.equal( ' NOTE: Do not remove me.' ); + expect( commit.notes ).to.deep.equal( notes ); + } ); + + // See: https://github.com/ckeditor/ckeditor5/issues/7495. + it( 'removes duplicated scoped notes from the footer', () => { + const rawCommit = { + type: 'Other (table)', + subject: 'Extracted `TableMouse` plugin from `TableSelection` plugin. Closes #6757.', + merge: 'Merge pull request #7355 from ckeditor/i/6757', + header: 'Other (table): Extracted `TableMouse` plugin from `TableSelection` plugin. Closes #6757.', + body: null, + footer: 'MINOR BREAKING CHANGE (table): The `TableNavigation` plugin renamed to `TableKeyboard`.', + notes: [ + { + title: 'MINOR BREAKING CHANGE', + text: '(table): The `TableNavigation` plugin renamed to `TableKeyboard`.' + } + ], + references: [], + mentions: [], + revert: null, + hash: '4d2f5f9b9f298601b332f304da66333c52673cb8' + }; + + const commit = transformCommit( rawCommit ); + + expect( commit.footer ).to.equal( null ); + } ); + + // See: https://github.com/ckeditor/ckeditor5/issues/7489. + describe( 'internal merge commits', () => { + const mergeCommitsToIgnore = [ + 'Merge branch \'stable\'', + 'Merge branch \'master\'', + 'Merge branch \'release\'', + 'Merge \'stable\' into \'master\'', + 'Merge \'master\' into \'release\'', + 'Merge \'release\' into \'stable\'', + 'Merge branch \'stable\' into \'master\'', + 'Merge branch \'master\' into \'release\'', + 'Merge branch \'release\' into \'stable\'', + 'Merge branch \'stable\' into master', + 'Merge branch \'master\' into release', + 'Merge branch \'release\' into stable', + 'Merge branch stable into \'master\'', + 'Merge branch master into \'release\'', + 'Merge branch release into \'stable\'', + 'Merge branch stable into master', + 'Merge branch master into release', + 'Merge branch release into stable', + 'Merge remote-tracking branch \'origin/master\' into i/6788-feature-branch', + 'Merge branch \'master\' into i/6788-feature-branch', + 'Merge branch master into i/6788-feature-branch' + ]; + + const validMergeCommits = [ + 'Merge pull request #7485 from ckeditor/i/6788-feature-branch', + 'Merge branch \'i/6788-feature-branch\'', + 'Merge branch i/6788-feature-branch' + ]; + + for ( const commitTitle of mergeCommitsToIgnore ) { + it( `ignores a commit: "${ commitTitle }"`, () => { + const rawCommit = { + merge: commitTitle, + header: '-hash-', + body: '575e00bc8ece48826adefe226c4fb1fe071c73a7', + notes: [] + }; - it( 'returns a new instance of object instead od modifying passed one', () => { - const notes = [ - { title: 'BREAKING CHANGES', text: 'Foo-Text', scope: null }, - { title: 'BREAKING CHANGES', text: 'Bar-Text', scope: null } - ]; + expect( transformCommit( rawCommit ) ).to.equal( undefined ); + } ); + } + + for ( const commitTitle of validMergeCommits ) { + it( `does not ignore a commit: "${ commitTitle }"`, () => { + const rawCommit = { + merge: commitTitle, + header: '-hash-', + body: '575e00bc8ece48826adefe226c4fb1fe071c73a7', + notes: [] + }; + + expect( transformCommit( rawCommit ) ).to.not.equal( undefined ); + } ); + } + } ); + describe( '"Closes" references - merging into single entry', () => { + it( 'works for #id pattern', () => { const rawCommit = { hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix.', + header: 'Fix: Simple fix. Closes #1. Closes #2. Closes #3.', type: 'Fix', - subject: 'Simple fix.', + subject: 'Simple fix. Closes #1. Closes #2. Closes #3.', body: null, footer: null, - notes + notes: [] }; const commit = transformCommit( rawCommit ); - // Notes cannot be the same but they should be equal. - expect( commit.notes ).to.not.equal( rawCommit.notes ); - expect( commit.notes ).to.deep.equal( rawCommit.notes ); + const expectedSubject = 'Simple fix. Closes ' + + '[#1](https://github.com/ckeditor/ckeditor5-dev/issues/1), ' + + '[#2](https://github.com/ckeditor/ckeditor5-dev/issues/2), ' + + '[#3](https://github.com/ckeditor/ckeditor5-dev/issues/3).'; + + expect( commit.subject ).to.equal( expectedSubject ); } ); - it( 'returns files that were changed with the commit', () => { + it( 'works for org/repo#id pattern', () => { const rawCommit = { hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix.', + header: 'Fix: Simple fix. Closes ckeditor/ckeditor5#1. Closes ckeditor/ckeditor5#2. Closes ckeditor/ckeditor5#3.', type: 'Fix', - subject: 'Simple fix.', + subject: 'Simple fix. Closes ckeditor/ckeditor5#1. Closes ckeditor/ckeditor5#2. Closes ckeditor/ckeditor5#3.', body: null, footer: null, notes: [] }; - const files = [ - 'a/b/y.txt', - 'c/d/z.md' - ]; - - stubs.getChangedFilesForCommit.returns( files ); - const commit = transformCommit( rawCommit ); - expect( stubs.getChangedFilesForCommit.calledOnce ).to.equal( true ); - expect( commit.files ).to.deep.equal( files ); + const expectedSubject = 'Simple fix. Closes ' + + '[ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1), ' + + '[ckeditor/ckeditor5#2](https://github.com/ckeditor/ckeditor5/issues/2), ' + + '[ckeditor/ckeditor5#3](https://github.com/ckeditor/ckeditor5/issues/3).'; + + expect( commit.subject ).to.equal( expectedSubject ); } ); - it( 'returns non-public commit', () => { + it( 'works for mixed #id and org/repo#id patterns, starting with #id', () => { const rawCommit = { - hash: '684997d', - header: 'Docs: README.', - type: 'Docs', - subject: 'README.', + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix. Closes #1. Closes #2. Closes ckeditor/ckeditor5#3.', + type: 'Fix', + subject: 'Simple fix. Closes #1. Closes #2. Closes ckeditor/ckeditor5#3.', body: null, footer: null, notes: [] }; - const newCommit = transformCommit( rawCommit ); + const commit = transformCommit( rawCommit ); + + const expectedSubject = 'Simple fix. Closes ' + + '[#1](https://github.com/ckeditor/ckeditor5-dev/issues/1), ' + + '[#2](https://github.com/ckeditor/ckeditor5-dev/issues/2), ' + + '[ckeditor/ckeditor5#3](https://github.com/ckeditor/ckeditor5/issues/3).'; - expect( newCommit ).to.not.equal( undefined ); + expect( commit.subject ).to.equal( expectedSubject ); } ); - it( 'groups "BREAKING CHANGES" and "BREAKING CHANGE" as "MAJOR BREAKING CHANGES"', () => { + it( 'works for mixed #id and org/repo#id patterns, starting with org/repo#id', () => { const rawCommit = { hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix.', + header: 'Fix: Simple fix. Closes ckeditor/ckeditor5#1. Closes ckeditor/ckeditor5#2. Closes #3.', type: 'Fix', - subject: 'Simple fix.', + subject: 'Simple fix. Closes ckeditor/ckeditor5#1. Closes ckeditor/ckeditor5#2. Closes #3.', body: null, footer: null, - notes: [ - { title: 'BREAKING CHANGE', text: 'Note 1.' }, - { title: 'BREAKING CHANGES', text: 'Note 2.' } - ] + notes: [] }; - const transformCommit = transformCommitFactory( { - useExplicitBreakingChangeGroups: true - } ); const commit = transformCommit( rawCommit ); - expect( commit.notes[ 0 ].title ).to.equal( 'MAJOR BREAKING CHANGES' ); - expect( commit.notes[ 1 ].title ).to.equal( 'MAJOR BREAKING CHANGES' ); + const expectedSubject = 'Simple fix. Closes ' + + '[ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1), ' + + '[ckeditor/ckeditor5#2](https://github.com/ckeditor/ckeditor5/issues/2), ' + + '[#3](https://github.com/ckeditor/ckeditor5-dev/issues/3).'; + + expect( commit.subject ).to.equal( expectedSubject ); } ); - it( 'makes proper links in the commit subject', () => { + it( 'does no touch the "See #" references.', () => { const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix: Simple fix. See ckeditor/ckeditor5#1. Thanks to @CKEditor. Closes #2.', + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix. Closes #1. Closes #2. See #3, #.', type: 'Fix', - subject: 'Simple fix. See ckeditor/ckeditor5#1. Thanks to @CKEditor. Closes #2.', + subject: 'Simple fix. Closes #1. Closes #2. See #3, #4.', body: null, footer: null, notes: [] @@ -179,1388 +550,983 @@ describe( 'dev-release-tools/utils', () => { const commit = transformCommit( rawCommit ); - const expectedSubject = 'Simple fix. ' + - 'See [ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1). ' + - 'Thanks to [@CKEditor](https://github.com/CKEditor). ' + - 'Closes [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2).'; + const expectedSubject = 'Simple fix. Closes ' + + '[#1](https://github.com/ckeditor/ckeditor5-dev/issues/1), ' + + '[#2](https://github.com/ckeditor/ckeditor5-dev/issues/2). ' + + 'See [#3](https://github.com/ckeditor/ckeditor5-dev/issues/3), ' + + '[#4](https://github.com/ckeditor/ckeditor5-dev/issues/4).'; expect( commit.subject ).to.equal( expectedSubject ); } ); - it( 'makes proper links in the commit body', () => { + it( 'does not replace paths with hash as github issue', () => { const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix: Simple fix. Closes #2.', + hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + header: 'Fix: Simple fix. Closes i/am/path5#1.', type: 'Fix', - subject: 'Simple fix. Closes #2', - body: 'See ckeditor/ckeditor5#1. Thanks to @CKEditor. Read more #2.', + subject: 'Fix: Simple fix. Closes i/am/path5#1.', + body: null, footer: null, notes: [] }; const commit = transformCommit( rawCommit ); - // Remember about the indent in commit body. - const expectedBody = ' See [ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1). ' + - 'Thanks to [@CKEditor](https://github.com/CKEditor). ' + - 'Read more [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2).'; - - expect( commit.body ).to.equal( expectedBody ); + expect( commit.subject ).to.equal( 'Fix: Simple fix. Closes i/am/path5#1.' ); } ); + } ); - it( 'makes proper links in the commit notes', () => { + describe( 'scopes', () => { + it( 'returns null if the scope is being missed', () => { const rawCommit = { hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix: Simple fix. Closes #2.', + header: 'Fix: Simple fix.', type: 'Fix', - subject: 'Simple fix. Closes #2', + subject: 'Simple fix.', body: null, footer: null, - notes: [ - { - title: 'BREAKING CHANGES', - text: 'See ckeditor/ckeditor5#1. Thanks to @CKEditor.' - }, - { - title: 'BREAKING CHANGES', - text: 'Read more #2.' - } - ] + notes: [] }; const commit = transformCommit( rawCommit ); - const expectedFirstNoteText = 'See [ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1). ' + - 'Thanks to [@CKEditor](https://github.com/CKEditor).'; - - // eslint-disable-next-line max-len - const expectedSecondNodeText = 'Read more [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2).'; - - expect( commit.notes[ 0 ].text ).to.equal( expectedFirstNoteText ); - expect( commit.notes[ 1 ].text ).to.equal( expectedSecondNodeText ); + expect( commit.scope ).to.be.equal( null ); } ); - it( 'attaches additional commit description with correct indent', () => { - const commitDescription = [ - '* Release task - rebuilt module for collecting dependencies to release.', - '* Used `semver` package for bumping the version (instead of a custom module).' - ]; - - const commitDescriptionWithIndents = [ - ' * Release task - rebuilt module for collecting dependencies to release.', - ' * Used `semver` package for bumping the version (instead of a custom module).' - ].join( '\n' ); - + it( 'extracts the scope from the commit type', () => { const rawCommit = { - header: 'Feature: Introduced a brand new release tools with a new set of requirements. See #64.', - hash: 'dea35014ab610be0c2150343c6a8a68620cfe5ad', - body: commitDescription.join( '\n' ), + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Fix (package): Simple fix.', + type: 'Fix (package)', + subject: 'Simple fix.', + body: null, footer: null, - mentions: [], - type: 'Feature', - subject: 'Introduced a brand new release tools with a new set of requirements. See #64.', notes: [] }; const commit = transformCommit( rawCommit ); - expect( commit.type ).to.equal( 'Features' ); - expect( commit.subject ).to.equal( 'Introduced a brand new release tools with a new set of requirements. ' + - 'See [#64](https://github.com/ckeditor/ckeditor5-dev/issues/64).' ); - expect( commit.body ).to.equal( commitDescriptionWithIndents ); + expect( commit.scope ).to.be.an( 'Array' ); + expect( commit.scope.length ).to.equal( 1 ); + expect( commit.scope[ 0 ] ).to.equal( 'package' ); + expect( commit.rawType ).to.equal( 'Fix' ); } ); - it( 'removes references to issues', () => { + it( 'works with multi-scoped changes (commit type)', () => { const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix.', - type: 'Fix', + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature (foo, bar): Simple fix.', + type: 'Feature (foo, bar)', subject: 'Simple fix.', body: null, footer: null, - notes: [], - references: [ - { issue: '11' }, - { issue: '12' } - ] + notes: [] }; const commit = transformCommit( rawCommit ); - expect( commit.references ).to.equal( undefined ); + expect( commit ).to.be.an( 'Array' ); + expect( commit.length ).to.equal( 2 ); + + expect( commit[ 0 ].scope ).to.be.an( 'Array' ); + expect( commit[ 0 ].scope.length ).to.equal( 1 ); + expect( commit[ 0 ].scope[ 0 ] ).to.equal( 'bar' ); + expect( commit[ 0 ].rawType ).to.equal( 'Feature' ); + + expect( commit[ 1 ].scope ).to.be.an( 'Array' ); + expect( commit[ 1 ].scope.length ).to.equal( 1 ); + expect( commit[ 1 ].scope[ 0 ] ).to.equal( 'foo' ); + expect( commit[ 1 ].rawType ).to.equal( 'Feature' ); } ); - it( 'uses commit\'s footer as a commit\'s body when commit does not have additional notes', () => { + it( 'works with multi-scoped merge commit', () => { const rawCommit = { - hash: 'dea35014ab610be0c2150343c6a8a68620cfe5ad', - header: 'Feature: Introduced a brand new release tools with a new set of requirements.', - type: 'Feature', - subject: 'Introduced a brand new release tools with a new set of requirements.', + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature (foo, bar): Simple fix.', + type: 'Feature (foo, bar)', + subject: 'Simple fix.', + merge: 'Merge pull request #7355 from ckeditor/i/6757', body: null, - footer: 'Additional description has been parsed as a footer but it should be a body.', + footer: null, notes: [] }; const commit = transformCommit( rawCommit ); - expect( commit.body ).to.equal( - ' Additional description has been parsed as a footer but it should be a body.' - ); - expect( commit.footer ).to.equal( null ); + expect( commit ).to.be.an( 'Array' ); + expect( commit.length ).to.equal( 2 ); + + expect( commit[ 0 ].scope ).to.be.an( 'Array' ); + expect( commit[ 0 ].scope.length ).to.equal( 1 ); + expect( commit[ 0 ].scope[ 0 ] ).to.equal( 'bar' ); + expect( commit[ 0 ].rawType ).to.equal( 'Feature' ); + + expect( commit[ 1 ].scope ).to.be.an( 'Array' ); + expect( commit[ 1 ].scope.length ).to.equal( 1 ); + expect( commit[ 1 ].scope[ 0 ] ).to.equal( 'foo' ); + expect( commit[ 1 ].rawType ).to.equal( 'Feature' ); } ); - it( 'removes [skip ci] from the commit message', () => { + it( 'clones the commit properties for multi-scoped changes', () => { const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: README. [skip ci]', - type: 'Fix', - subject: 'README. [skip ci]', + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature (foo, bar): Simple fix.', + type: 'Feature (foo, bar)', + subject: 'Simple fix.', body: null, footer: null, - notes: [] + notes: [ + { + title: 'BREAKING CHANGES', + text: '(package): Foo.' + } + ] }; const commit = transformCommit( rawCommit ); - expect( commit.subject ).to.equal( 'README.' ); - } ); + expect( commit ).to.be.an( 'Array' ); + expect( commit.length ).to.equal( 2 ); + + expect( commit[ 0 ].scope ).to.be.an( 'Array' ); + expect( commit[ 1 ].scope ).to.be.an( 'Array' ); + expect( commit[ 0 ].scope ).to.not.equal( commit[ 1 ].scope ); - it( 'includes "repositoryUrl" where the commit has been done', () => { - const notes = [ - { title: 'Foo', text: 'Foo-Text' }, - { title: 'Bar', text: 'Bar-Text' } - ]; + expect( commit[ 0 ].files ).to.be.an( 'Array' ); + expect( commit[ 1 ].files ).to.be.an( 'Array' ); + expect( commit[ 0 ].files ).to.not.equal( commit[ 1 ].files ); + expect( commit[ 0 ].notes ).to.be.an( 'Array' ); + expect( commit[ 1 ].notes ).to.be.an( 'Array' ); + expect( commit[ 0 ].notes ).to.not.equal( commit[ 1 ].notes ); + + expect( commit[ 0 ].notes[ 0 ].scope ).to.be.an( 'Array' ); + expect( commit[ 0 ].notes[ 0 ].scope[ 0 ] ).to.equal( 'package' ); + expect( commit[ 1 ].notes ).to.be.an( 'Array' ); + } ); + + it( 'extracts the scope from notes', () => { const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', header: 'Fix: Simple fix.', type: 'Fix', subject: 'Simple fix.', body: null, footer: null, - notes + notes: [ + { + title: 'BREAKING CHANGES', + text: '(package): Foo.' + } + ] }; const commit = transformCommit( rawCommit ); - expect( commit.repositoryUrl ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + expect( commit.notes ).to.be.an( 'Array' ); + expect( commit.notes.length ).to.equal( 1 ); + expect( commit.notes[ 0 ] ).to.be.an( 'Object' ); + expect( commit.notes[ 0 ].text ).to.equal( 'Foo.' ); + expect( commit.notes[ 0 ].title ).to.equal( 'BREAKING CHANGES' ); + expect( commit.notes[ 0 ].scope ).to.be.an( 'Array' ); + expect( commit.notes[ 0 ].scope.length ).to.equal( 1 ); + expect( commit.notes[ 0 ].scope[ 0 ] ).to.equal( 'package' ); } ); - it( 'treats all "* BREAKING CHANGES" notes as "BREAKING CHANGE"', () => { + it( 'works with multi-scoped notes', () => { const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', header: 'Fix: Simple fix.', type: 'Fix', subject: 'Simple fix.', body: null, footer: null, notes: [ - { title: 'BREAKING CHANGE', text: 'Note 1.' }, - { title: 'BREAKING CHANGES', text: 'Note 2.' }, - { title: 'MAJOR BREAKING CHANGE', text: 'Note 3.' }, - { title: 'MAJOR BREAKING CHANGES', text: 'Note 4.' }, - { title: 'MINOR BREAKING CHANGE', text: 'Note 5.' }, - { title: 'MINOR BREAKING CHANGES', text: 'Note 6.' } + { + title: 'BREAKING CHANGES', + text: '(foo, bar): Package.' + } ] }; const commit = transformCommit( rawCommit ); + expect( commit.notes ).to.be.an( 'Array' ); + expect( commit.notes.length ).to.equal( 1 ); + expect( commit.notes[ 0 ] ).to.be.an( 'Object' ); + expect( commit.notes[ 0 ].text ).to.equal( 'Package.' ); expect( commit.notes[ 0 ].title ).to.equal( 'BREAKING CHANGES' ); - expect( commit.notes[ 1 ].title ).to.equal( 'BREAKING CHANGES' ); - expect( commit.notes[ 2 ].title ).to.equal( 'BREAKING CHANGES' ); - expect( commit.notes[ 3 ].title ).to.equal( 'BREAKING CHANGES' ); - expect( commit.notes[ 4 ].title ).to.equal( 'BREAKING CHANGES' ); - expect( commit.notes[ 5 ].title ).to.equal( 'BREAKING CHANGES' ); + expect( commit.notes[ 0 ].scope ).to.be.an( 'Array' ); + expect( commit.notes[ 0 ].scope.length ).to.equal( 2 ); + expect( commit.notes[ 0 ].scope[ 0 ] ).to.equal( 'bar' ); + expect( commit.notes[ 0 ].scope[ 1 ] ).to.equal( 'foo' ); } ); - it( 'removes duplicated notes from the footer', () => { - const notes = [ - { title: 'BREAKING CHANGES', text: 'Foo.', scope: null }, - { title: 'BREAKING CHANGES', text: 'Bar-Text.', scope: null } - ]; - + it( 'does not copy notes when processing multi-scoped commit (single entry)', () => { const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix.', - type: 'Fix', - subject: 'Simple fix.', - body: null, - footer: [ - 'BREAKING CHANGES: Foo.', - 'NOTE: Do not remove me.', - 'BREAKING CHANGES: Bar-Text.' - ].join( '\n' ), - notes + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Fix (scope1, scope2): Simple feature Closes #1.', + type: 'Fix (scope1, scope2)', + subject: 'Simple feature Closes #1.', + body: '', + footer: null, + notes: [ + { title: 'BREAKING CHANGE', text: 'Note 1.' }, + { title: 'MAJOR BREAKING CHANGES', text: 'Note 2.' } + ] }; const commit = transformCommit( rawCommit ); - expect( commit.body ).to.equal( ' NOTE: Do not remove me.' ); - expect( commit.notes ).to.deep.equal( notes ); + expect( commit.length ).to.equal( 2 ); + + expect( commit[ 0 ].scope ).to.be.an( 'Array' ); + expect( commit[ 0 ].scope.length ).to.equal( 1 ); + expect( commit[ 0 ].scope[ 0 ] ).to.equal( 'scope1' ); + expect( commit[ 0 ].rawType ).to.equal( 'Fix' ); + expect( commit[ 0 ].notes ).to.be.an( 'Array' ); + expect( commit[ 0 ].notes.length ).to.equal( 2 ); + expect( commit[ 0 ].notes[ 0 ].scope ).to.equal( null ); + expect( commit[ 0 ].notes[ 1 ].scope ).to.equal( null ); + + expect( commit[ 1 ].scope ).to.be.an( 'Array' ); + expect( commit[ 1 ].scope.length ).to.equal( 1 ); + expect( commit[ 1 ].scope[ 0 ] ).to.equal( 'scope2' ); + expect( commit[ 1 ].rawType ).to.equal( 'Fix' ); + expect( commit[ 1 ].notes ).to.be.an( 'Array' ); + expect( commit[ 1 ].notes.length ).to.equal( 0 ); } ); - // See: https://github.com/ckeditor/ckeditor5/issues/7495. - it( 'removes duplicated scoped notes from the footer', () => { + it( 'does not copy notes when processing multi-scoped commit (multi entries)', () => { const rawCommit = { - type: 'Other (table)', - subject: 'Extracted `TableMouse` plugin from `TableSelection` plugin. Closes #6757.', - merge: 'Merge pull request #7355 from ckeditor/i/6757', - header: 'Other (table): Extracted `TableMouse` plugin from `TableSelection` plugin. Closes #6757.', - body: null, - footer: 'MINOR BREAKING CHANGE (table): The `TableNavigation` plugin renamed to `TableKeyboard`.', + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Fix (scope1, scope2): Simple feature Closes #1.', + type: 'Fix (scope1, scope2)', + subject: 'Simple feature Closes #1.', + body: [ + 'Other (scope3, scope4): Simple other change. Closes ckeditor/ckeditor5#3. Closes #3.' + ].join( '\n' ), + footer: null, notes: [ - { - title: 'MINOR BREAKING CHANGE', - text: '(table): The `TableNavigation` plugin renamed to `TableKeyboard`.' - } - ], - references: [], - mentions: [], - revert: null, - hash: '4d2f5f9b9f298601b332f304da66333c52673cb8' + { title: 'BREAKING CHANGE', text: 'Note 1.' }, + { title: 'MAJOR BREAKING CHANGES', text: 'Note 2.' } + ] }; const commit = transformCommit( rawCommit ); - expect( commit.footer ).to.equal( null ); - } ); - - // See: https://github.com/ckeditor/ckeditor5/issues/7489. - describe( 'internal merge commits', () => { - const mergeCommitsToIgnore = [ - 'Merge branch \'stable\'', - 'Merge branch \'master\'', - 'Merge branch \'release\'', - 'Merge \'stable\' into \'master\'', - 'Merge \'master\' into \'release\'', - 'Merge \'release\' into \'stable\'', - 'Merge branch \'stable\' into \'master\'', - 'Merge branch \'master\' into \'release\'', - 'Merge branch \'release\' into \'stable\'', - 'Merge branch \'stable\' into master', - 'Merge branch \'master\' into release', - 'Merge branch \'release\' into stable', - 'Merge branch stable into \'master\'', - 'Merge branch master into \'release\'', - 'Merge branch release into \'stable\'', - 'Merge branch stable into master', - 'Merge branch master into release', - 'Merge branch release into stable', - 'Merge remote-tracking branch \'origin/master\' into i/6788-feature-branch', - 'Merge branch \'master\' into i/6788-feature-branch', - 'Merge branch master into i/6788-feature-branch' - ]; - - const validMergeCommits = [ - 'Merge pull request #7485 from ckeditor/i/6788-feature-branch', - 'Merge branch \'i/6788-feature-branch\'', - 'Merge branch i/6788-feature-branch' - ]; - - for ( const commitTitle of mergeCommitsToIgnore ) { - it( `ignores a commit: "${ commitTitle }"`, () => { - const rawCommit = { - merge: commitTitle, - header: '-hash-', - body: '575e00bc8ece48826adefe226c4fb1fe071c73a7', - notes: [] - }; - - expect( transformCommit( rawCommit ) ).to.equal( undefined ); - } ); - } - - for ( const commitTitle of validMergeCommits ) { - it( `does not ignore a commit: "${ commitTitle }"`, () => { - const rawCommit = { - merge: commitTitle, - header: '-hash-', - body: '575e00bc8ece48826adefe226c4fb1fe071c73a7', - notes: [] - }; - - expect( transformCommit( rawCommit ) ).to.not.equal( undefined ); - } ); - } + expect( commit.length ).to.equal( 4 ); + + expect( commit[ 0 ].scope ).to.be.an( 'Array' ); + expect( commit[ 0 ].scope.length ).to.equal( 1 ); + expect( commit[ 0 ].scope[ 0 ] ).to.equal( 'scope1' ); + expect( commit[ 0 ].rawType ).to.equal( 'Fix' ); + expect( commit[ 0 ].notes ).to.be.an( 'Array' ); + expect( commit[ 0 ].notes.length ).to.equal( 2 ); + + expect( commit[ 1 ].scope ).to.be.an( 'Array' ); + expect( commit[ 1 ].scope.length ).to.equal( 1 ); + expect( commit[ 1 ].scope[ 0 ] ).to.equal( 'scope2' ); + expect( commit[ 1 ].rawType ).to.equal( 'Fix' ); + expect( commit[ 1 ].notes ).to.be.an( 'Array' ); + expect( commit[ 1 ].notes.length ).to.equal( 0 ); + + expect( commit[ 2 ].scope ).to.be.an( 'Array' ); + expect( commit[ 2 ].scope.length ).to.equal( 1 ); + expect( commit[ 2 ].scope[ 0 ] ).to.equal( 'scope3' ); + expect( commit[ 2 ].rawType ).to.equal( 'Other' ); + expect( commit[ 2 ].notes ).to.be.an( 'Array' ); + expect( commit[ 2 ].notes.length ).to.equal( 0 ); + + expect( commit[ 3 ].scope ).to.be.an( 'Array' ); + expect( commit[ 3 ].scope.length ).to.equal( 1 ); + expect( commit[ 3 ].scope[ 0 ] ).to.equal( 'scope4' ); + expect( commit[ 3 ].rawType ).to.equal( 'Other' ); + expect( commit[ 3 ].notes ).to.be.an( 'Array' ); + expect( commit[ 3 ].notes.length ).to.equal( 0 ); } ); + } ); - describe( '"Closes" references - merging into single entry', () => { - it( 'works for #id pattern', () => { - const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix. Closes #1. Closes #2. Closes #3.', - type: 'Fix', - subject: 'Simple fix. Closes #1. Closes #2. Closes #3.', - body: null, - footer: null, - notes: [] - }; + describe( 'multi-entries commit', () => { + it( 'returns an array with all entries', () => { + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature (1).', + type: 'Feature', + subject: 'Simple feature (1).', + body: [ + 'Fix: Simple fix (2).', + '', + 'Other: Simple other change (3).' + ].join( '\n' ), + footer: null, + notes: [] + }; - const commit = transformCommit( rawCommit ); + const commits = transformCommit( rawCommit ); - const expectedSubject = 'Simple fix. Closes ' + - '[#1](https://github.com/ckeditor/ckeditor5-dev/issues/1), ' + - '[#2](https://github.com/ckeditor/ckeditor5-dev/issues/2), ' + - '[#3](https://github.com/ckeditor/ckeditor5-dev/issues/3).'; + expect( commits ).to.be.an( 'Array' ); + expect( commits.length ).to.equal( 3 ); - expect( commit.subject ).to.equal( expectedSubject ); + expect( commits[ 0 ] ).to.deep.equal( { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature (1).', + type: 'Features', + subject: 'Simple feature (1).', + body: '', + footer: null, + notes: [], + rawType: 'Feature', + files: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); - it( 'works for org/repo#id pattern', () => { - const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix. Closes ckeditor/ckeditor5#1. Closes ckeditor/ckeditor5#2. Closes ckeditor/ckeditor5#3.', - type: 'Fix', - subject: 'Simple fix. Closes ckeditor/ckeditor5#1. Closes ckeditor/ckeditor5#2. Closes ckeditor/ckeditor5#3.', - body: null, - footer: null, - notes: [] - }; - - const commit = transformCommit( rawCommit ); - - const expectedSubject = 'Simple fix. Closes ' + - '[ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1), ' + - '[ckeditor/ckeditor5#2](https://github.com/ckeditor/ckeditor5/issues/2), ' + - '[ckeditor/ckeditor5#3](https://github.com/ckeditor/ckeditor5/issues/3).'; - - expect( commit.subject ).to.equal( expectedSubject ); + expect( commits[ 1 ] ).to.deep.equal( { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Fix: Simple fix (2).', + type: 'Bug fixes', + subject: 'Simple fix (2).', + body: '', + revert: null, + merge: null, + footer: null, + notes: [], + rawType: 'Fix', + files: [], + mentions: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); - it( 'works for mixed #id and org/repo#id patterns, starting with #id', () => { - const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix. Closes #1. Closes #2. Closes ckeditor/ckeditor5#3.', - type: 'Fix', - subject: 'Simple fix. Closes #1. Closes #2. Closes ckeditor/ckeditor5#3.', - body: null, - footer: null, - notes: [] - }; - - const commit = transformCommit( rawCommit ); - - const expectedSubject = 'Simple fix. Closes ' + - '[#1](https://github.com/ckeditor/ckeditor5-dev/issues/1), ' + - '[#2](https://github.com/ckeditor/ckeditor5-dev/issues/2), ' + - '[ckeditor/ckeditor5#3](https://github.com/ckeditor/ckeditor5/issues/3).'; - - expect( commit.subject ).to.equal( expectedSubject ); + expect( commits[ 2 ] ).to.deep.equal( { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Other: Simple other change (3).', + type: 'Other changes', + subject: 'Simple other change (3).', + body: '', + revert: null, + merge: null, + footer: null, + notes: [], + rawType: 'Other', + files: [], + mentions: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); + } ); - it( 'works for mixed #id and org/repo#id patterns, starting with org/repo#id', () => { - const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix. Closes ckeditor/ckeditor5#1. Closes ckeditor/ckeditor5#2. Closes #3.', - type: 'Fix', - subject: 'Simple fix. Closes ckeditor/ckeditor5#1. Closes ckeditor/ckeditor5#2. Closes #3.', - body: null, - footer: null, - notes: [] - }; + it( 'preserves the description of the first commit', () => { + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature (1).', + type: 'Feature', + subject: 'Simple feature (1).', + body: [ + 'Lorem ipsum. #1', + '', + 'Fix: Simple fix (2).', + '', + 'Second lorem ipsum. #2', + '', + 'Other: Other simple change (3).' + ].join( '\n' ), + footer: null, + notes: [] + }; - const commit = transformCommit( rawCommit ); + const commits = transformCommit( rawCommit ); - const expectedSubject = 'Simple fix. Closes ' + - '[ckeditor/ckeditor5#1](https://github.com/ckeditor/ckeditor5/issues/1), ' + - '[ckeditor/ckeditor5#2](https://github.com/ckeditor/ckeditor5/issues/2), ' + - '[#3](https://github.com/ckeditor/ckeditor5-dev/issues/3).'; + expect( commits ).to.be.an( 'Array' ); + expect( commits.length ).to.equal( 3 ); - expect( commit.subject ).to.equal( expectedSubject ); + expect( commits[ 0 ] ).to.deep.equal( { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature (1).', + type: 'Features', + subject: 'Simple feature (1).', + body: ' Lorem ipsum. [#1](https://github.com/ckeditor/ckeditor5-dev/issues/1)', + footer: null, + notes: [], + rawType: 'Feature', + files: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); - it( 'does no touch the "See #" references.', () => { - const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix. Closes #1. Closes #2. See #3, #.', - type: 'Fix', - subject: 'Simple fix. Closes #1. Closes #2. See #3, #4.', - body: null, - footer: null, - notes: [] - }; - - const commit = transformCommit( rawCommit ); - - const expectedSubject = 'Simple fix. Closes ' + - '[#1](https://github.com/ckeditor/ckeditor5-dev/issues/1), ' + - '[#2](https://github.com/ckeditor/ckeditor5-dev/issues/2). ' + - 'See [#3](https://github.com/ckeditor/ckeditor5-dev/issues/3), ' + - '[#4](https://github.com/ckeditor/ckeditor5-dev/issues/4).'; - - expect( commit.subject ).to.equal( expectedSubject ); + expect( commits[ 1 ] ).to.deep.equal( { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Fix: Simple fix (2).', + type: 'Bug fixes', + subject: 'Simple fix (2).', + body: ' Second lorem ipsum. [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2)', + revert: null, + merge: null, + footer: null, + notes: [], + rawType: 'Fix', + files: [], + mentions: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); - it( 'does not replace paths with hash as github issue', () => { - const rawCommit = { - hash: '684997d0eb2eca76b9e058fb1c3fa00b50059cdc', - header: 'Fix: Simple fix. Closes i/am/path5#1.', - type: 'Fix', - subject: 'Fix: Simple fix. Closes i/am/path5#1.', - body: null, - footer: null, - notes: [] - }; - - const commit = transformCommit( rawCommit ); - - expect( commit.subject ).to.equal( 'Fix: Simple fix. Closes i/am/path5#1.' ); + expect( commits[ 2 ] ).to.deep.equal( { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Other: Other simple change (3).', + type: 'Other changes', + subject: 'Other simple change (3).', + body: '', + revert: null, + merge: null, + footer: null, + notes: [], + rawType: 'Other', + files: [], + mentions: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); } ); - describe( 'scopes', () => { - it( 'returns null if the scope is being missed', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix: Simple fix.', - type: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [] - }; - - const commit = transformCommit( rawCommit ); - - expect( commit.scope ).to.be.equal( null ); - } ); + it( 'adds a dot at the subject if missing in new commit', () => { + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature (1).', + type: 'Feature', + subject: 'Simple feature (1).', + body: [ + 'Fix: Simple fix (2)' + ].join( '\n' ), + footer: null, + notes: [] + }; - it( 'extracts the scope from the commit type', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix (package): Simple fix.', - type: 'Fix (package)', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [] - }; + const commits = transformCommit( rawCommit ); - const commit = transformCommit( rawCommit ); + expect( commits ).to.be.an( 'Array' ); + expect( commits.length ).to.equal( 2 ); - expect( commit.scope ).to.be.an( 'Array' ); - expect( commit.scope.length ).to.equal( 1 ); - expect( commit.scope[ 0 ] ).to.equal( 'package' ); - expect( commit.rawType ).to.equal( 'Fix' ); - } ); + expect( commits[ 1 ].subject ).to.equal( 'Simple fix (2).' ); + } ); - it( 'works with multi-scoped changes (commit type)', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature (foo, bar): Simple fix.', - type: 'Feature (foo, bar)', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [] - }; + it( 'copies an array with changed files across all commits', () => { + const files = [ 'a', 'b', 'c' ]; - const commit = transformCommit( rawCommit ); + vi.mocked( getChangedFilesForCommit ).mockReturnValue( files ); - expect( commit ).to.be.an( 'Array' ); - expect( commit.length ).to.equal( 2 ); + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature (1).', + type: 'Feature', + subject: 'Simple feature (1).', + body: [ + 'Fix: Simple fix (2)', + '', + 'Other: Simple other change (3).' + ].join( '\n' ), + footer: null, + notes: [] + }; - expect( commit[ 0 ].scope ).to.be.an( 'Array' ); - expect( commit[ 0 ].scope.length ).to.equal( 1 ); - expect( commit[ 0 ].scope[ 0 ] ).to.equal( 'bar' ); - expect( commit[ 0 ].rawType ).to.equal( 'Feature' ); + const commits = transformCommit( rawCommit ); - expect( commit[ 1 ].scope ).to.be.an( 'Array' ); - expect( commit[ 1 ].scope.length ).to.equal( 1 ); - expect( commit[ 1 ].scope[ 0 ] ).to.equal( 'foo' ); - expect( commit[ 1 ].rawType ).to.equal( 'Feature' ); - } ); + expect( commits[ 0 ].files ).to.equal( files ); + expect( commits[ 1 ].files ).to.equal( files ); + expect( commits[ 2 ].files ).to.equal( files ); + } ); - it( 'works with multi-scoped merge commit', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature (foo, bar): Simple fix.', - type: 'Feature (foo, bar)', - subject: 'Simple fix.', - merge: 'Merge pull request #7355 from ckeditor/i/6757', - body: null, - footer: null, - notes: [] - }; + it( 'works with non-public commits', () => { + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature (1).', + type: 'Feature', + subject: 'Simple feature (1).', + body: [ + 'Docs: Simple docs change (2)', + '', + 'Internal: Simple internal change (3).' + ].join( '\n' ), + footer: null, + notes: [] + }; - const commit = transformCommit( rawCommit ); + const commits = transformCommit( rawCommit ); - expect( commit ).to.be.an( 'Array' ); - expect( commit.length ).to.equal( 2 ); + expect( commits ).to.be.an( 'Array' ); + expect( commits.length ).to.equal( 3 ); - expect( commit[ 0 ].scope ).to.be.an( 'Array' ); - expect( commit[ 0 ].scope.length ).to.equal( 1 ); - expect( commit[ 0 ].scope[ 0 ] ).to.equal( 'bar' ); - expect( commit[ 0 ].rawType ).to.equal( 'Feature' ); + expect( commits[ 0 ].isPublicCommit ).to.equal( true ); + expect( commits[ 1 ].isPublicCommit ).to.equal( false ); + expect( commits[ 2 ].isPublicCommit ).to.equal( false ); + } ); - expect( commit[ 1 ].scope ).to.be.an( 'Array' ); - expect( commit[ 1 ].scope.length ).to.equal( 1 ); - expect( commit[ 1 ].scope[ 0 ] ).to.equal( 'foo' ); - expect( commit[ 1 ].rawType ).to.equal( 'Feature' ); - } ); + it( 'handles scoped and non-scoped changes', () => { + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature (1).', + type: 'Feature', + subject: 'Simple feature (1).', + body: [ + 'Fix (foo): Simple fix (2).', + '', + 'Other: Simple other change (3).', + '', + 'Feature (foo, bar): Simple other change (4).' + ].join( '\n' ), + footer: null, + notes: [] + }; - it( 'clones the commit properties for multi-scoped changes', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature (foo, bar): Simple fix.', - type: 'Feature (foo, bar)', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [ - { - title: 'BREAKING CHANGES', - text: '(package): Foo.' - } - ] - }; + const commits = transformCommit( rawCommit ); - const commit = transformCommit( rawCommit ); + expect( commits ).to.be.an( 'Array' ); + expect( commits.length ).to.equal( 5 ); - expect( commit ).to.be.an( 'Array' ); - expect( commit.length ).to.equal( 2 ); + expect( commits[ 0 ].scope ).to.equal( null ); + expect( commits[ 1 ].scope ).to.deep.equal( [ 'foo' ] ); + expect( commits[ 2 ].scope ).to.equal( null ); + expect( commits[ 3 ].scope ).to.deep.equal( [ 'bar' ] ); + expect( commits[ 4 ].scope ).to.deep.equal( [ 'foo' ] ); + } ); - expect( commit[ 0 ].scope ).to.be.an( 'Array' ); - expect( commit[ 1 ].scope ).to.be.an( 'Array' ); - expect( commit[ 0 ].scope ).to.not.equal( commit[ 1 ].scope ); + it( 'merges "Closes" references in multi-entries commit', () => { + const rawCommit = { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature Closes #1.', + type: 'Feature', + subject: 'Simple feature Closes #1.', + body: [ + 'Fix: Simple fix. Closes #2. Closes ckeditor/ckeditor5#2. See ckeditor/ckeditor5#1000.', + '', + 'Other: Simple other change. Closes ckeditor/ckeditor5#3. Closes #3.' + ].join( '\n' ), + footer: null, + notes: [] + }; - expect( commit[ 0 ].files ).to.be.an( 'Array' ); - expect( commit[ 1 ].files ).to.be.an( 'Array' ); - expect( commit[ 0 ].files ).to.not.equal( commit[ 1 ].files ); + const commits = transformCommit( rawCommit ); - expect( commit[ 0 ].notes ).to.be.an( 'Array' ); - expect( commit[ 1 ].notes ).to.be.an( 'Array' ); - expect( commit[ 0 ].notes ).to.not.equal( commit[ 1 ].notes ); + expect( commits ).to.be.an( 'Array' ); + expect( commits.length ).to.equal( 3 ); - expect( commit[ 0 ].notes[ 0 ].scope ).to.be.an( 'Array' ); - expect( commit[ 0 ].notes[ 0 ].scope[ 0 ] ).to.equal( 'package' ); - expect( commit[ 1 ].notes ).to.be.an( 'Array' ); + expect( commits[ 0 ] ).to.deep.equal( { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Feature: Simple feature Closes #1.', + type: 'Features', + subject: 'Simple feature Closes [#1](https://github.com/ckeditor/ckeditor5-dev/issues/1).', + body: '', + footer: null, + notes: [], + rawType: 'Feature', + files: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); - it( 'extracts the scope from notes', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix: Simple fix.', - type: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [ - { - title: 'BREAKING CHANGES', - text: '(package): Foo.' - } - ] - }; - - const commit = transformCommit( rawCommit ); - - expect( commit.notes ).to.be.an( 'Array' ); - expect( commit.notes.length ).to.equal( 1 ); - expect( commit.notes[ 0 ] ).to.be.an( 'Object' ); - expect( commit.notes[ 0 ].text ).to.equal( 'Foo.' ); - expect( commit.notes[ 0 ].title ).to.equal( 'BREAKING CHANGES' ); - expect( commit.notes[ 0 ].scope ).to.be.an( 'Array' ); - expect( commit.notes[ 0 ].scope.length ).to.equal( 1 ); - expect( commit.notes[ 0 ].scope[ 0 ] ).to.equal( 'package' ); + expect( commits[ 1 ] ).to.deep.equal( { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Fix: Simple fix. Closes #2. Closes ckeditor/ckeditor5#2. See ckeditor/ckeditor5#1000.', + type: 'Bug fixes', + subject: 'Simple fix. Closes [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2), ' + + '[ckeditor/ckeditor5#2](https://github.com/ckeditor/ckeditor5/issues/2). ' + + 'See [ckeditor/ckeditor5#1000](https://github.com/ckeditor/ckeditor5/issues/1000).', + body: '', + revert: null, + merge: null, + footer: null, + notes: [], + rawType: 'Fix', + files: [], + mentions: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); - it( 'works with multi-scoped notes', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix: Simple fix.', - type: 'Fix', - subject: 'Simple fix.', - body: null, - footer: null, - notes: [ - { - title: 'BREAKING CHANGES', - text: '(foo, bar): Package.' - } - ] - }; - - const commit = transformCommit( rawCommit ); - - expect( commit.notes ).to.be.an( 'Array' ); - expect( commit.notes.length ).to.equal( 1 ); - expect( commit.notes[ 0 ] ).to.be.an( 'Object' ); - expect( commit.notes[ 0 ].text ).to.equal( 'Package.' ); - expect( commit.notes[ 0 ].title ).to.equal( 'BREAKING CHANGES' ); - expect( commit.notes[ 0 ].scope ).to.be.an( 'Array' ); - expect( commit.notes[ 0 ].scope.length ).to.equal( 2 ); - expect( commit.notes[ 0 ].scope[ 0 ] ).to.equal( 'bar' ); - expect( commit.notes[ 0 ].scope[ 1 ] ).to.equal( 'foo' ); + expect( commits[ 2 ] ).to.deep.equal( { + hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', + header: 'Other: Simple other change. Closes ckeditor/ckeditor5#3. Closes #3.', + type: 'Other changes', + subject: 'Simple other change. Closes [ckeditor/ckeditor5#3](https://github.com/ckeditor/ckeditor5/issues/3), ' + + '[#3](https://github.com/ckeditor/ckeditor5-dev/issues/3).', + body: '', + revert: null, + merge: null, + footer: null, + notes: [], + rawType: 'Other', + files: [], + mentions: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); + } ); + } ); - it( 'does not copy notes when processing multi-scoped commit (single entry)', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix (scope1, scope2): Simple feature Closes #1.', - type: 'Fix (scope1, scope2)', - subject: 'Simple feature Closes #1.', - body: '', - footer: null, - notes: [ - { title: 'BREAKING CHANGE', text: 'Note 1.' }, - { title: 'MAJOR BREAKING CHANGES', text: 'Note 2.' } - ] - }; + describe( 'squash merge commit', () => { + it( 'removes the squash commit part from results', () => { + const rawCommit = { + type: null, + subject: null, + merge: null, + header: 'A squash pull request change (#111)', + body: 'Fix (scope-1): Description 1.\n' + + '\n' + + 'Other (scope-2): Description 2.\n' + + '\n' + + 'Internal (scope-3): Description 3.', + footer: '', + notes: [], + references: [], + mentions: [], + revert: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' + }; - const commit = transformCommit( rawCommit ); - - expect( commit.length ).to.equal( 2 ); - - expect( commit[ 0 ].scope ).to.be.an( 'Array' ); - expect( commit[ 0 ].scope.length ).to.equal( 1 ); - expect( commit[ 0 ].scope[ 0 ] ).to.equal( 'scope1' ); - expect( commit[ 0 ].rawType ).to.equal( 'Fix' ); - expect( commit[ 0 ].notes ).to.be.an( 'Array' ); - expect( commit[ 0 ].notes.length ).to.equal( 2 ); - expect( commit[ 0 ].notes[ 0 ].scope ).to.equal( null ); - expect( commit[ 0 ].notes[ 1 ].scope ).to.equal( null ); - - expect( commit[ 1 ].scope ).to.be.an( 'Array' ); - expect( commit[ 1 ].scope.length ).to.equal( 1 ); - expect( commit[ 1 ].scope[ 0 ] ).to.equal( 'scope2' ); - expect( commit[ 1 ].rawType ).to.equal( 'Fix' ); - expect( commit[ 1 ].notes ).to.be.an( 'Array' ); - expect( commit[ 1 ].notes.length ).to.equal( 0 ); - } ); + const commits = transformCommit( rawCommit ); - it( 'does not copy notes when processing multi-scoped commit (multi entries)', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix (scope1, scope2): Simple feature Closes #1.', - type: 'Fix (scope1, scope2)', - subject: 'Simple feature Closes #1.', - body: [ - 'Other (scope3, scope4): Simple other change. Closes ckeditor/ckeditor5#3. Closes #3.' - ].join( '\n' ), - footer: null, - notes: [ - { title: 'BREAKING CHANGE', text: 'Note 1.' }, - { title: 'MAJOR BREAKING CHANGES', text: 'Note 2.' } - ] - }; + expect( commits ).to.be.an( 'Array' ); + expect( commits ).to.lengthOf( 3 ); - const commit = transformCommit( rawCommit ); - - expect( commit.length ).to.equal( 4 ); - - expect( commit[ 0 ].scope ).to.be.an( 'Array' ); - expect( commit[ 0 ].scope.length ).to.equal( 1 ); - expect( commit[ 0 ].scope[ 0 ] ).to.equal( 'scope1' ); - expect( commit[ 0 ].rawType ).to.equal( 'Fix' ); - expect( commit[ 0 ].notes ).to.be.an( 'Array' ); - expect( commit[ 0 ].notes.length ).to.equal( 2 ); - - expect( commit[ 1 ].scope ).to.be.an( 'Array' ); - expect( commit[ 1 ].scope.length ).to.equal( 1 ); - expect( commit[ 1 ].scope[ 0 ] ).to.equal( 'scope2' ); - expect( commit[ 1 ].rawType ).to.equal( 'Fix' ); - expect( commit[ 1 ].notes ).to.be.an( 'Array' ); - expect( commit[ 1 ].notes.length ).to.equal( 0 ); - - expect( commit[ 2 ].scope ).to.be.an( 'Array' ); - expect( commit[ 2 ].scope.length ).to.equal( 1 ); - expect( commit[ 2 ].scope[ 0 ] ).to.equal( 'scope3' ); - expect( commit[ 2 ].rawType ).to.equal( 'Other' ); - expect( commit[ 2 ].notes ).to.be.an( 'Array' ); - expect( commit[ 2 ].notes.length ).to.equal( 0 ); - - expect( commit[ 3 ].scope ).to.be.an( 'Array' ); - expect( commit[ 3 ].scope.length ).to.equal( 1 ); - expect( commit[ 3 ].scope[ 0 ] ).to.equal( 'scope4' ); - expect( commit[ 3 ].rawType ).to.equal( 'Other' ); - expect( commit[ 3 ].notes ).to.be.an( 'Array' ); - expect( commit[ 3 ].notes.length ).to.equal( 0 ); + expect( commits[ 0 ] ).to.deep.equal( { + revert: null, + merge: 'A squash pull request change (#111)', + footer: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + files: [], + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', + notes: [], + mentions: [], + rawType: 'Fix', + scope: [ 'scope-1' ], + isPublicCommit: true, + type: 'Bug fixes', + header: 'Fix (scope-1): Description 1.', + subject: 'Description 1.', + body: '' } ); - } ); - - describe( 'multi-entries commit', () => { - it( 'returns an array with all entries', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature (1).', - type: 'Feature', - subject: 'Simple feature (1).', - body: [ - 'Fix: Simple fix (2).', - '', - 'Other: Simple other change (3).' - ].join( '\n' ), - footer: null, - notes: [] - }; - const commits = transformCommit( rawCommit ); - - expect( commits ).to.be.an( 'Array' ); - expect( commits.length ).to.equal( 3 ); - - expect( commits[ 0 ] ).to.deep.equal( { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature (1).', - type: 'Features', - subject: 'Simple feature (1).', - body: '', - footer: null, - notes: [], - rawType: 'Feature', - files: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); - - expect( commits[ 1 ] ).to.deep.equal( { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix: Simple fix (2).', - type: 'Bug fixes', - subject: 'Simple fix (2).', - body: '', - revert: null, - merge: null, - footer: null, - notes: [], - rawType: 'Fix', - files: [], - mentions: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); - - expect( commits[ 2 ] ).to.deep.equal( { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Other: Simple other change (3).', - type: 'Other changes', - subject: 'Simple other change (3).', - body: '', - revert: null, - merge: null, - footer: null, - notes: [], - rawType: 'Other', - files: [], - mentions: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); + expect( commits[ 1 ] ).to.deep.equal( { + revert: null, + merge: null, + footer: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + files: [], + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', + notes: [], + mentions: [], + rawType: 'Other', + scope: [ 'scope-2' ], + isPublicCommit: true, + type: 'Other changes', + header: 'Other (scope-2): Description 2.', + subject: 'Description 2.', + body: '' } ); - it( 'preserves the description of the first commit', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature (1).', - type: 'Feature', - subject: 'Simple feature (1).', - body: [ - 'Lorem ipsum. #1', - '', - 'Fix: Simple fix (2).', - '', - 'Second lorem ipsum. #2', - '', - 'Other: Other simple change (3).' - ].join( '\n' ), - footer: null, - notes: [] - }; - - const commits = transformCommit( rawCommit ); - - expect( commits ).to.be.an( 'Array' ); - expect( commits.length ).to.equal( 3 ); - - expect( commits[ 0 ] ).to.deep.equal( { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature (1).', - type: 'Features', - subject: 'Simple feature (1).', - body: ' Lorem ipsum. [#1](https://github.com/ckeditor/ckeditor5-dev/issues/1)', - footer: null, - notes: [], - rawType: 'Feature', - files: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); - - expect( commits[ 1 ] ).to.deep.equal( { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix: Simple fix (2).', - type: 'Bug fixes', - subject: 'Simple fix (2).', - body: ' Second lorem ipsum. [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2)', - revert: null, - merge: null, - footer: null, - notes: [], - rawType: 'Fix', - files: [], - mentions: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); - - expect( commits[ 2 ] ).to.deep.equal( { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Other: Other simple change (3).', - type: 'Other changes', - subject: 'Other simple change (3).', - body: '', - revert: null, - merge: null, - footer: null, - notes: [], - rawType: 'Other', - files: [], - mentions: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); + expect( commits[ 2 ] ).to.deep.equal( { + revert: null, + merge: null, + footer: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + files: [], + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', + notes: [], + mentions: [], + rawType: 'Internal', + scope: [ 'scope-3' ], + isPublicCommit: false, + header: 'Internal (scope-3): Description 3.', + subject: 'Description 3.', + body: '' } ); + } ); - it( 'adds a dot at the subject if missing in new commit', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature (1).', - type: 'Feature', - subject: 'Simple feature (1).', - body: [ - 'Fix: Simple fix (2)' - ].join( '\n' ), - footer: null, - notes: [] - }; + it( 'processes breaking change notes from the removed squash commit', () => { + const rawCommit = { + type: null, + subject: null, + merge: null, + header: 'A squash pull request change (#111)', + body: 'Fix (scope-1): Description 1.\n' + + '\n' + + 'Other (scope-2): Description 2.\n' + + '\n' + + 'Internal (scope-3): Description 3.', + footer: 'MINOR BREAKING CHANGE (scope-1): BC 1.\n' + + '\n' + + 'MINOR BREAKING CHANGE (scope-2): BC 2.', + notes: [ + { + title: 'MINOR BREAKING CHANGE', + text: '(scope-1): BC 1.' + }, + { + title: 'MINOR BREAKING CHANGE', + text: '(scope-2): BC 2.' + } + ], + references: [], + mentions: [], + revert: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' + }; - const commits = transformCommit( rawCommit ); + const commits = transformCommit( rawCommit ); - expect( commits ).to.be.an( 'Array' ); - expect( commits.length ).to.equal( 2 ); + expect( commits ).to.be.an( 'Array' ); + expect( commits ).to.lengthOf( 3 ); - expect( commits[ 1 ].subject ).to.equal( 'Simple fix (2).' ); + expect( commits[ 0 ] ).to.deep.equal( { + revert: null, + merge: 'A squash pull request change (#111)', + footer: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + files: [], + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', + notes: [ + { + scope: [ + 'scope-1' + ], + text: 'BC 1.', + title: 'BREAKING CHANGES' + }, + { + scope: [ + 'scope-2' + ], + text: 'BC 2.', + title: 'BREAKING CHANGES' + } + ], + mentions: [], + rawType: 'Fix', + scope: [ 'scope-1' ], + isPublicCommit: true, + type: 'Bug fixes', + header: 'Fix (scope-1): Description 1.', + subject: 'Description 1.', + body: '' } ); + expect( commits[ 1 ].notes ).to.deep.equal( [] ); + expect( commits[ 2 ].notes ).to.deep.equal( [] ); + } ); - it( 'copies an array with changed files across all commits', () => { - const files = [ 'a', 'b', 'c' ]; - - stubs.getChangedFilesForCommit.returns( files ); + it( 'does not remove the squash commit if all changes are marked as internal', () => { + const rawCommit = { + type: null, + subject: null, + merge: null, + header: 'A squash pull request change (#111)', + body: 'Internal (scope-1): Description 1.\n' + + '\n' + + 'Internal (scope-2): Description 2.\n' + + '\n' + + 'Internal (scope-3): Description 3.', + footer: 'MINOR BREAKING CHANGE (scope-1): BC 1.\n' + + '\n' + + 'MINOR BREAKING CHANGE (scope-2): BC 2.', + notes: [ + { + title: 'MINOR BREAKING CHANGE', + text: '(scope-1): BC 1.' + }, + { + title: 'MINOR BREAKING CHANGE', + text: '(scope-2): BC 2.' + } + ], + references: [], + mentions: [], + revert: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' + }; - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature (1).', - type: 'Feature', - subject: 'Simple feature (1).', - body: [ - 'Fix: Simple fix (2)', - '', - 'Other: Simple other change (3).' - ].join( '\n' ), - footer: null, - notes: [] - }; + const commits = transformCommit( rawCommit ); - const commits = transformCommit( rawCommit ); + expect( commits ).to.be.an( 'Array' ); + expect( commits ).to.lengthOf( 4 ); - expect( commits[ 0 ].files ).to.equal( files ); - expect( commits[ 1 ].files ).to.equal( files ); - expect( commits[ 2 ].files ).to.equal( files ); + expect( commits[ 0 ] ).to.deep.equal( { + type: null, + subject: null, + merge: null, + header: 'A squash pull request change (#111)', + body: '', + footer: 'MINOR BREAKING CHANGE (scope-1): BC 1.\n' + + '\n' + + 'MINOR BREAKING CHANGE (scope-2): BC 2.', + notes: [ + { + text: '(scope-1): BC 1.', + title: 'MINOR BREAKING CHANGE' + }, + { + text: '(scope-2): BC 2.', + title: 'MINOR BREAKING CHANGE' + } + ], + references: [], + mentions: [], + revert: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + rawType: undefined, + files: [], + scope: undefined, + isPublicCommit: false, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); - - it( 'works with non-public commits', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature (1).', - type: 'Feature', - subject: 'Simple feature (1).', - body: [ - 'Docs: Simple docs change (2)', - '', - 'Internal: Simple internal change (3).' - ].join( '\n' ), - footer: null, - notes: [] - }; - - const commits = transformCommit( rawCommit ); - - expect( commits ).to.be.an( 'Array' ); - expect( commits.length ).to.equal( 3 ); - - expect( commits[ 0 ].isPublicCommit ).to.equal( true ); - expect( commits[ 1 ].isPublicCommit ).to.equal( false ); - expect( commits[ 2 ].isPublicCommit ).to.equal( false ); + expect( commits[ 1 ] ).to.deep.equal( { + revert: null, + merge: null, + footer: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + files: [], + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', + notes: [], + mentions: [], + rawType: 'Internal', + scope: [ 'scope-1' ], + isPublicCommit: false, + header: 'Internal (scope-1): Description 1.', + subject: 'Description 1.', + body: '' } ); - - it( 'handles scoped and non-scoped changes', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature (1).', - type: 'Feature', - subject: 'Simple feature (1).', - body: [ - 'Fix (foo): Simple fix (2).', - '', - 'Other: Simple other change (3).', - '', - 'Feature (foo, bar): Simple other change (4).' - ].join( '\n' ), - footer: null, - notes: [] - }; - - const commits = transformCommit( rawCommit ); - - expect( commits ).to.be.an( 'Array' ); - expect( commits.length ).to.equal( 5 ); - - expect( commits[ 0 ].scope ).to.equal( null ); - expect( commits[ 1 ].scope ).to.deep.equal( [ 'foo' ] ); - expect( commits[ 2 ].scope ).to.equal( null ); - expect( commits[ 3 ].scope ).to.deep.equal( [ 'bar' ] ); - expect( commits[ 4 ].scope ).to.deep.equal( [ 'foo' ] ); + expect( commits[ 2 ] ).to.deep.equal( { + revert: null, + merge: null, + footer: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + files: [], + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', + notes: [], + mentions: [], + rawType: 'Internal', + scope: [ 'scope-2' ], + isPublicCommit: false, + header: 'Internal (scope-2): Description 2.', + subject: 'Description 2.', + body: '' } ); - - it( 'merges "Closes" references in multi-entries commit', () => { - const rawCommit = { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature Closes #1.', - type: 'Feature', - subject: 'Simple feature Closes #1.', - body: [ - 'Fix: Simple fix. Closes #2. Closes ckeditor/ckeditor5#2. See ckeditor/ckeditor5#1000.', - '', - 'Other: Simple other change. Closes ckeditor/ckeditor5#3. Closes #3.' - ].join( '\n' ), - footer: null, - notes: [] - }; - - const commits = transformCommit( rawCommit ); - - expect( commits ).to.be.an( 'Array' ); - expect( commits.length ).to.equal( 3 ); - - expect( commits[ 0 ] ).to.deep.equal( { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Feature: Simple feature Closes #1.', - type: 'Features', - subject: 'Simple feature Closes [#1](https://github.com/ckeditor/ckeditor5-dev/issues/1).', - body: '', - footer: null, - notes: [], - rawType: 'Feature', - files: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); - - expect( commits[ 1 ] ).to.deep.equal( { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Fix: Simple fix. Closes #2. Closes ckeditor/ckeditor5#2. See ckeditor/ckeditor5#1000.', - type: 'Bug fixes', - subject: 'Simple fix. Closes [#2](https://github.com/ckeditor/ckeditor5-dev/issues/2), ' + - '[ckeditor/ckeditor5#2](https://github.com/ckeditor/ckeditor5/issues/2). ' + - 'See [ckeditor/ckeditor5#1000](https://github.com/ckeditor/ckeditor5/issues/1000).', - body: '', - revert: null, - merge: null, - footer: null, - notes: [], - rawType: 'Fix', - files: [], - mentions: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); - - expect( commits[ 2 ] ).to.deep.equal( { - hash: '76b9e058fb1c3fa00b50059cdc684997d0eb2eca', - header: 'Other: Simple other change. Closes ckeditor/ckeditor5#3. Closes #3.', - type: 'Other changes', - subject: 'Simple other change. Closes [ckeditor/ckeditor5#3](https://github.com/ckeditor/ckeditor5/issues/3), ' + - '[#3](https://github.com/ckeditor/ckeditor5-dev/issues/3).', - body: '', - revert: null, - merge: null, - footer: null, - notes: [], - rawType: 'Other', - files: [], - mentions: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); + expect( commits[ 3 ] ).to.deep.equal( { + revert: null, + merge: null, + footer: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + files: [], + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', + notes: [], + mentions: [], + rawType: 'Internal', + scope: [ 'scope-3' ], + isPublicCommit: false, + header: 'Internal (scope-3): Description 3.', + subject: 'Description 3.', + body: '' } ); } ); - describe( 'squash merge commit', () => { - it( 'removes the squash commit part from results', () => { - const rawCommit = { - type: null, - subject: null, - merge: null, - header: 'A squash pull request change (#111)', - body: 'Fix (scope-1): Description 1.\n' + - '\n' + - 'Other (scope-2): Description 2.\n' + - '\n' + - 'Internal (scope-3): Description 3.', - footer: '', - notes: [], - references: [], - mentions: [], - revert: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' - }; + it( 'does not remove the squash commit if it is a valid message', () => { + const rawCommit = { + type: 'Fix', + subject: 'A squash pull request change (#111)', + merge: null, + header: 'Fix: A squash pull request change (#111)', + body: 'Internal (scope-1): Description 1.', + footer: '', + notes: [], + references: [], + mentions: [], + revert: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' + }; - const commits = transformCommit( rawCommit ); - - expect( commits ).to.be.an( 'Array' ); - expect( commits ).to.lengthOf( 3 ); - - expect( commits[ 0 ] ).to.deep.equal( { - revert: null, - merge: 'A squash pull request change (#111)', - footer: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - files: [], - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', - notes: [], - mentions: [], - rawType: 'Fix', - scope: [ 'scope-1' ], - isPublicCommit: true, - type: 'Bug fixes', - header: 'Fix (scope-1): Description 1.', - subject: 'Description 1.', - body: '' - } ); - - expect( commits[ 1 ] ).to.deep.equal( { - revert: null, - merge: null, - footer: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - files: [], - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', - notes: [], - mentions: [], - rawType: 'Other', - scope: [ 'scope-2' ], - isPublicCommit: true, - type: 'Other changes', - header: 'Other (scope-2): Description 2.', - subject: 'Description 2.', - body: '' - } ); - - expect( commits[ 2 ] ).to.deep.equal( { - revert: null, - merge: null, - footer: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - files: [], - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', - notes: [], - mentions: [], - rawType: 'Internal', - scope: [ 'scope-3' ], - isPublicCommit: false, - header: 'Internal (scope-3): Description 3.', - subject: 'Description 3.', - body: '' - } ); - } ); + const commits = transformCommit( rawCommit ); - it( 'processes breaking change notes from the removed squash commit', () => { - const rawCommit = { - type: null, - subject: null, - merge: null, - header: 'A squash pull request change (#111)', - body: 'Fix (scope-1): Description 1.\n' + - '\n' + - 'Other (scope-2): Description 2.\n' + - '\n' + - 'Internal (scope-3): Description 3.', - footer: 'MINOR BREAKING CHANGE (scope-1): BC 1.\n' + - '\n' + - 'MINOR BREAKING CHANGE (scope-2): BC 2.', - notes: [ - { - title: 'MINOR BREAKING CHANGE', - text: '(scope-1): BC 1.' - }, - { - title: 'MINOR BREAKING CHANGE', - text: '(scope-2): BC 2.' - } - ], - references: [], - mentions: [], - revert: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' - }; + expect( commits ).to.be.an( 'Array' ); + expect( commits ).to.lengthOf( 2 ); - const commits = transformCommit( rawCommit ); - - expect( commits ).to.be.an( 'Array' ); - expect( commits ).to.lengthOf( 3 ); - - expect( commits[ 0 ] ).to.deep.equal( { - revert: null, - merge: 'A squash pull request change (#111)', - footer: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - files: [], - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', - notes: [ - { - scope: [ - 'scope-1' - ], - text: 'BC 1.', - title: 'BREAKING CHANGES' - }, - { - scope: [ - 'scope-2' - ], - text: 'BC 2.', - title: 'BREAKING CHANGES' - } - ], - mentions: [], - rawType: 'Fix', - scope: [ 'scope-1' ], - isPublicCommit: true, - type: 'Bug fixes', - header: 'Fix (scope-1): Description 1.', - subject: 'Description 1.', - body: '' - } ); - expect( commits[ 1 ].notes ).to.deep.equal( [] ); - expect( commits[ 2 ].notes ).to.deep.equal( [] ); + expect( commits[ 0 ] ).to.deep.equal( { + type: 'Bug fixes', + rawType: 'Fix', + subject: 'A squash pull request change ([#111](https://github.com/ckeditor/ckeditor5-dev/issues/111)).', + merge: null, + header: 'Fix: A squash pull request change (#111)', + body: '', + footer: '', + notes: [], + mentions: [], + revert: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + files: [], + scope: null, + isPublicCommit: true, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); - - it( 'does not remove the squash commit if all changes are marked as internal', () => { - const rawCommit = { - type: null, - subject: null, - merge: null, - header: 'A squash pull request change (#111)', - body: 'Internal (scope-1): Description 1.\n' + - '\n' + - 'Internal (scope-2): Description 2.\n' + - '\n' + - 'Internal (scope-3): Description 3.', - footer: 'MINOR BREAKING CHANGE (scope-1): BC 1.\n' + - '\n' + - 'MINOR BREAKING CHANGE (scope-2): BC 2.', - notes: [ - { - title: 'MINOR BREAKING CHANGE', - text: '(scope-1): BC 1.' - }, - { - title: 'MINOR BREAKING CHANGE', - text: '(scope-2): BC 2.' - } - ], - references: [], - mentions: [], - revert: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' - }; - - const commits = transformCommit( rawCommit ); - - expect( commits ).to.be.an( 'Array' ); - expect( commits ).to.lengthOf( 4 ); - - expect( commits[ 0 ] ).to.deep.equal( { - type: null, - subject: null, - merge: null, - header: 'A squash pull request change (#111)', - body: '', - footer: 'MINOR BREAKING CHANGE (scope-1): BC 1.\n' + - '\n' + - 'MINOR BREAKING CHANGE (scope-2): BC 2.', - notes: [ - { - text: '(scope-1): BC 1.', - title: 'MINOR BREAKING CHANGE' - }, - { - text: '(scope-2): BC 2.', - title: 'MINOR BREAKING CHANGE' - } - ], - references: [], - mentions: [], - revert: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - rawType: undefined, - files: [], - scope: undefined, - isPublicCommit: false, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); - expect( commits[ 1 ] ).to.deep.equal( { - revert: null, - merge: null, - footer: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - files: [], - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', - notes: [], - mentions: [], - rawType: 'Internal', - scope: [ 'scope-1' ], - isPublicCommit: false, - header: 'Internal (scope-1): Description 1.', - subject: 'Description 1.', - body: '' - } ); - expect( commits[ 2 ] ).to.deep.equal( { - revert: null, - merge: null, - footer: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - files: [], - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', - notes: [], - mentions: [], - rawType: 'Internal', - scope: [ 'scope-2' ], - isPublicCommit: false, - header: 'Internal (scope-2): Description 2.', - subject: 'Description 2.', - body: '' - } ); - expect( commits[ 3 ] ).to.deep.equal( { - revert: null, - merge: null, - footer: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - files: [], - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', - notes: [], - mentions: [], - rawType: 'Internal', - scope: [ 'scope-3' ], - isPublicCommit: false, - header: 'Internal (scope-3): Description 3.', - subject: 'Description 3.', - body: '' - } ); + expect( commits[ 1 ] ).to.deep.equal( { + revert: null, + merge: null, + footer: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + files: [], + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', + notes: [], + mentions: [], + rawType: 'Internal', + scope: [ 'scope-1' ], + isPublicCommit: false, + header: 'Internal (scope-1): Description 1.', + subject: 'Description 1.', + body: '' } ); + } ); - it( 'does not remove the squash commit if it is a valid message', () => { - const rawCommit = { - type: 'Fix', - subject: 'A squash pull request change (#111)', - merge: null, - header: 'Fix: A squash pull request change (#111)', - body: 'Internal (scope-1): Description 1.', - footer: '', - notes: [], - references: [], - mentions: [], - revert: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' - }; - - const commits = transformCommit( rawCommit ); - - expect( commits ).to.be.an( 'Array' ); - expect( commits ).to.lengthOf( 2 ); - - expect( commits[ 0 ] ).to.deep.equal( { - type: 'Bug fixes', - rawType: 'Fix', - subject: 'A squash pull request change ([#111](https://github.com/ckeditor/ckeditor5-dev/issues/111)).', - merge: null, - header: 'Fix: A squash pull request change (#111)', - body: '', - footer: '', - notes: [], - mentions: [], - revert: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - files: [], - scope: null, - isPublicCommit: true, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); - expect( commits[ 1 ] ).to.deep.equal( { - revert: null, - merge: null, - footer: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - files: [], - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev', - notes: [], - mentions: [], - rawType: 'Internal', - scope: [ 'scope-1' ], - isPublicCommit: false, - header: 'Internal (scope-1): Description 1.', - subject: 'Description 1.', - body: '' - } ); - } ); + it( 'processes a title including various non-letter symbols', () => { + const rawCommit = { + type: null, + subject: null, + merge: null, + header: 'A squash pull (#12) request change! (#111)', + body: 'Just details.', + footer: '', + notes: [], + references: [], + mentions: [], + revert: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' + }; - it( 'processes a title including various non-letter symbols', () => { - const rawCommit = { - type: null, - subject: null, - merge: null, - header: 'A squash pull (#12) request change! (#111)', - body: 'Just details.', - footer: '', - notes: [], - references: [], - mentions: [], - revert: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee' - }; + const commit = transformCommit( rawCommit ); - const commit = transformCommit( rawCommit ); - - expect( commit ).to.be.an( 'Object' ); - expect( commit ).to.deep.equal( { - type: null, - subject: null, - merge: null, - header: 'A squash pull (#12) request change! (#111)', - body: 'Just details.', - footer: '', - notes: [], - references: [], - mentions: [], - revert: null, - hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', - rawType: undefined, - files: [], - scope: undefined, - isPublicCommit: false, - repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' - } ); + expect( commit ).to.be.an( 'Object' ); + expect( commit ).to.deep.equal( { + type: null, + subject: null, + merge: null, + header: 'A squash pull (#12) request change! (#111)', + body: 'Just details.', + footer: '', + notes: [], + references: [], + mentions: [], + revert: null, + hash: 'bb24d87e46a9f4675eabfa97e247ee7f58debeee', + rawType: undefined, + files: [], + scope: undefined, + isPublicCommit: false, + repositoryUrl: 'https://github.com/ckeditor/ckeditor5-dev' } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/transformcommitutils.js b/packages/ckeditor5-dev-release-tools/tests/utils/transformcommitutils.js index 37f3087f4..b33b44b69 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/transformcommitutils.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/transformcommitutils.js @@ -3,279 +3,250 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect, vi } from 'vitest'; +import getPackageJson from '../../lib/utils/getpackagejson.js'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); +import * as transformCommitUtils from '../../lib/utils/transformcommitutils.js'; -describe( 'dev-release-tools/utils', () => { - let transformCommitUtils, sandbox, stubs; +vi.mock( '../../lib/utils/getpackagejson.js' ); - describe( 'transformCommitUtils', () => { - beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - getPackageJson: sandbox.stub() - }; - - transformCommitUtils = proxyquire( '../../lib/utils/transformcommitutils', { - './getpackagejson': stubs.getPackageJson - } ); +describe( 'transformCommitUtils', () => { + describe( 'availableTypes', () => { + it( 'should be defined', () => { + expect( transformCommitUtils.availableCommitTypes ).to.be.a( 'Map' ); } ); + } ); - afterEach( () => { - sandbox.restore(); + describe( 'typesOrder', () => { + it( 'should be defined', () => { + expect( transformCommitUtils.typesOrder ).to.be.a( 'Object' ); } ); + } ); - describe( 'availableTypes', () => { - it( 'should be defined', () => { - expect( transformCommitUtils.availableCommitTypes ).to.be.a( 'Map' ); - } ); + describe( 'MULTI_ENTRIES_COMMIT_REGEXP', () => { + it( 'should be defined', () => { + expect( transformCommitUtils.MULTI_ENTRIES_COMMIT_REGEXP ).to.be.a( 'RegExp' ); } ); + } ); - describe( 'typesOrder', () => { - it( 'should be defined', () => { - expect( transformCommitUtils.typesOrder ).to.be.a( 'Object' ); - } ); + describe( 'getTypeOrder()', () => { + it( 'returns proper values for commit groups', () => { + expect( transformCommitUtils.getTypeOrder( 'Features' ) ).to.equal( 1 ); + expect( transformCommitUtils.getTypeOrder( 'Bug fixes' ) ).to.equal( 2 ); + expect( transformCommitUtils.getTypeOrder( 'Other changes' ) ).to.equal( 3 ); } ); - describe( 'MULTI_ENTRIES_COMMIT_REGEXP', () => { - it( 'should be defined', () => { - expect( transformCommitUtils.MULTI_ENTRIES_COMMIT_REGEXP ).to.be.a( 'RegExp' ); - } ); + it( 'returns proper values for commit groups when they ends with additional link', () => { + expect( transformCommitUtils.getTypeOrder( 'Features [ℹ](url)' ) ).to.equal( 1 ); + expect( transformCommitUtils.getTypeOrder( 'Bug fixes [ℹ](url)' ) ).to.equal( 2 ); + expect( transformCommitUtils.getTypeOrder( 'Other changes [ℹ](url)' ) ).to.equal( 3 ); } ); - describe( 'getTypeOrder()', () => { - it( 'returns proper values for commit groups', () => { - expect( transformCommitUtils.getTypeOrder( 'Features' ) ).to.equal( 1 ); - expect( transformCommitUtils.getTypeOrder( 'Bug fixes' ) ).to.equal( 2 ); - expect( transformCommitUtils.getTypeOrder( 'Other changes' ) ).to.equal( 3 ); - } ); + it( 'returns proper values for note groups', () => { + expect( transformCommitUtils.getTypeOrder( 'MAJOR BREAKING CHANGES' ) ).to.equal( 1 ); + expect( transformCommitUtils.getTypeOrder( 'MINOR BREAKING CHANGES' ) ).to.equal( 2 ); + expect( transformCommitUtils.getTypeOrder( 'BREAKING CHANGES' ) ).to.equal( 3 ); + } ); - it( 'returns proper values for commit groups when they ends with additional link', () => { - expect( transformCommitUtils.getTypeOrder( 'Features [ℹ](url)' ) ).to.equal( 1 ); - expect( transformCommitUtils.getTypeOrder( 'Bug fixes [ℹ](url)' ) ).to.equal( 2 ); - expect( transformCommitUtils.getTypeOrder( 'Other changes [ℹ](url)' ) ).to.equal( 3 ); - } ); + it( 'returns proper values for note groups when they ends with additional link', () => { + expect( transformCommitUtils.getTypeOrder( 'MAJOR BREAKING CHANGES [ℹ](url)' ) ).to.equal( 1 ); + expect( transformCommitUtils.getTypeOrder( 'MINOR BREAKING CHANGES [ℹ](url)' ) ).to.equal( 2 ); + expect( transformCommitUtils.getTypeOrder( 'BREAKING CHANGES [ℹ](url)' ) ).to.equal( 3 ); + } ); - it( 'returns proper values for note groups', () => { - expect( transformCommitUtils.getTypeOrder( 'MAJOR BREAKING CHANGES' ) ).to.equal( 1 ); - expect( transformCommitUtils.getTypeOrder( 'MINOR BREAKING CHANGES' ) ).to.equal( 2 ); - expect( transformCommitUtils.getTypeOrder( 'BREAKING CHANGES' ) ).to.equal( 3 ); - } ); + it( 'returns default value for unknown type', () => { + expect( transformCommitUtils.getTypeOrder( 'Foo' ) ).to.equal( 10 ); + expect( transformCommitUtils.getTypeOrder( 'Bar' ) ).to.equal( 10 ); - it( 'returns proper values for note groups when they ends with additional link', () => { - expect( transformCommitUtils.getTypeOrder( 'MAJOR BREAKING CHANGES [ℹ](url)' ) ).to.equal( 1 ); - expect( transformCommitUtils.getTypeOrder( 'MINOR BREAKING CHANGES [ℹ](url)' ) ).to.equal( 2 ); - expect( transformCommitUtils.getTypeOrder( 'BREAKING CHANGES [ℹ](url)' ) ).to.equal( 3 ); - } ); + expect( transformCommitUtils.getTypeOrder( 'Foo ℹ' ) ).to.equal( 10 ); + expect( transformCommitUtils.getTypeOrder( 'Bar ℹ' ) ).to.equal( 10 ); + } ); + } ); - it( 'returns default value for unknown type', () => { - expect( transformCommitUtils.getTypeOrder( 'Foo' ) ).to.equal( 10 ); - expect( transformCommitUtils.getTypeOrder( 'Bar' ) ).to.equal( 10 ); + describe( 'linkToGithubUser()', () => { + it( 'makes a link to GitHub profile if a user was mentioned in a comment', () => { + expect( transformCommitUtils.linkToGithubUser( 'Foo @CKSource Bar' ) ) + .to.equal( 'Foo [@CKSource](https://github.com/CKSource) Bar' ); + } ); - expect( transformCommitUtils.getTypeOrder( 'Foo ℹ' ) ).to.equal( 10 ); - expect( transformCommitUtils.getTypeOrder( 'Bar ℹ' ) ).to.equal( 10 ); - } ); + it( 'makes a link to GitHub profile if a user was mentioned in a comment at the beginning', () => { + expect( transformCommitUtils.linkToGithubUser( '@CKSource Bar' ) ) + .to.equal( '[@CKSource](https://github.com/CKSource) Bar' ); } ); - describe( 'linkToGithubUser()', () => { - it( 'makes a link to GitHub profile if a user was mentioned in a comment', () => { - expect( transformCommitUtils.linkToGithubUser( 'Foo @CKSource Bar' ) ) - .to.equal( 'Foo [@CKSource](https://github.com/CKSource) Bar' ); - } ); + it( 'makes a link to GitHub profile if a user was mentioned at the beginning of a line', () => { + expect( transformCommitUtils.linkToGithubUser( 'Foo\n@CKSource Bar' ) ) + .to.equal( 'Foo\n[@CKSource](https://github.com/CKSource) Bar' ); + } ); - it( 'makes a link to GitHub profile if a user was mentioned in a comment at the beginning', () => { - expect( transformCommitUtils.linkToGithubUser( '@CKSource Bar' ) ) - .to.equal( '[@CKSource](https://github.com/CKSource) Bar' ); - } ); + it( 'makes a link to GitHub profile if a user was mentioned in a comment at the ending', () => { + expect( transformCommitUtils.linkToGithubUser( 'Bar @CKSource' ) ) + .to.equal( 'Bar [@CKSource](https://github.com/CKSource)' ); + } ); - it( 'makes a link to GitHub profile if a user was mentioned at the beginning of a line', () => { - expect( transformCommitUtils.linkToGithubUser( 'Foo\n@CKSource Bar' ) ) - .to.equal( 'Foo\n[@CKSource](https://github.com/CKSource) Bar' ); - } ); + it( 'makes a link to GitHub profile if a user was mentioned in a bracket', () => { + expect( transformCommitUtils.linkToGithubUser( 'Bar (@CKSource)' ) ) + .to.equal( 'Bar ([@CKSource](https://github.com/CKSource))' ); + } ); - it( 'makes a link to GitHub profile if a user was mentioned in a comment at the ending', () => { - expect( transformCommitUtils.linkToGithubUser( 'Bar @CKSource' ) ) - .to.equal( 'Bar [@CKSource](https://github.com/CKSource)' ); - } ); + it( 'does nothing if a comment contains scoped package name', () => { + expect( transformCommitUtils.linkToGithubUser( 'Foo @ckeditor/ckeditor5-foo Bar' ) ) + .to.equal( 'Foo @ckeditor/ckeditor5-foo Bar' ); + } ); - it( 'makes a link to GitHub profile if a user was mentioned in a bracket', () => { - expect( transformCommitUtils.linkToGithubUser( 'Bar (@CKSource)' ) ) - .to.equal( 'Bar ([@CKSource](https://github.com/CKSource))' ); - } ); + it( 'does nothing if an email is inside the comment', () => { + expect( transformCommitUtils.linkToGithubUser( 'Foo foo@bar.com Bar' ) ) + .to.equal( 'Foo foo@bar.com Bar' ); + } ); - it( 'does nothing if a comment contains scoped package name', () => { - expect( transformCommitUtils.linkToGithubUser( 'Foo @ckeditor/ckeditor5-foo Bar' ) ) - .to.equal( 'Foo @ckeditor/ckeditor5-foo Bar' ); - } ); + it( 'does nothing if a user is already linked', () => { + expect( transformCommitUtils.linkToGithubUser( 'Foo [@bar](https://github.com/bar) Bar' ) ) + .to.equal( 'Foo [@bar](https://github.com/bar) Bar' ); + } ); + } ); - it( 'does nothing if an email is inside the comment', () => { - expect( transformCommitUtils.linkToGithubUser( 'Foo foo@bar.com Bar' ) ) - .to.equal( 'Foo foo@bar.com Bar' ); + describe( 'linkToGithubIssue()', () => { + it( 'replaces "#ID" with a link to GitHub issue (packageJson.repository as a string)', () => { + vi.mocked( getPackageJson ).mockReturnValue( { + name: 'test-package', + repository: 'https://github.com/ckeditor/ckeditor5-dev' } ); - it( 'does nothing if a user is already linked', () => { - expect( transformCommitUtils.linkToGithubUser( 'Foo [@bar](https://github.com/bar) Bar' ) ) - .to.equal( 'Foo [@bar](https://github.com/bar) Bar' ); - } ); + expect( transformCommitUtils.linkToGithubIssue( 'Some issue #1.' ) ) + .to.equal( 'Some issue [#1](https://github.com/ckeditor/ckeditor5-dev/issues/1).' ); } ); - describe( 'linkToGithubIssue()', () => { - it( 'replaces "#ID" with a link to GitHub issue (packageJson.repository as a string)', () => { - stubs.getPackageJson.returns( { - name: 'test-package', - repository: 'https://github.com/ckeditor/ckeditor5-dev' - } ); + it( 'replaces "organization/repository#id" with a link to the issue in specified repository', () => { + expect( transformCommitUtils.linkToGithubIssue( 'ckeditor/ckeditor5-dev#1' ) ) + .to.equal( '[ckeditor/ckeditor5-dev#1](https://github.com/ckeditor/ckeditor5-dev/issues/1)' ); + } ); - expect( transformCommitUtils.linkToGithubIssue( 'Some issue #1.' ) ) - .to.equal( 'Some issue [#1](https://github.com/ckeditor/ckeditor5-dev/issues/1).' ); - } ); + it( 'does not make a link from a comment which is a path', () => { + expect( transformCommitUtils.linkToGithubIssue( 'i/am/a/path#1' ) ) + .to.equal( 'i/am/a/path#1' ); + } ); - it( 'replaces "organization/repository#id" with a link to the issue in specified repository', () => { - expect( transformCommitUtils.linkToGithubIssue( 'ckeditor/ckeditor5-dev#1' ) ) - .to.equal( '[ckeditor/ckeditor5-dev#1](https://github.com/ckeditor/ckeditor5-dev/issues/1)' ); - } ); + it( 'does not make a link if a comment does not match to "organization/repository"', () => { + expect( transformCommitUtils.linkToGithubIssue( 'ckeditor/ckeditor5-dev/' ) ) + .to.equal( 'ckeditor/ckeditor5-dev/' ); + } ); - it( 'does not make a link from a comment which is a path', () => { - expect( transformCommitUtils.linkToGithubIssue( 'i/am/a/path#1' ) ) - .to.equal( 'i/am/a/path#1' ); - } ); + it( 'does not make a link from a comment which does not contain the issue id', () => { + expect( transformCommitUtils.linkToGithubIssue( 'ckeditor/ckeditor5-dev#' ) ) + .to.equal( 'ckeditor/ckeditor5-dev#' ); + } ); - it( 'does not make a link if a comment does not match to "organization/repository"', () => { - expect( transformCommitUtils.linkToGithubIssue( 'ckeditor/ckeditor5-dev/' ) ) - .to.equal( 'ckeditor/ckeditor5-dev/' ); - } ); + it( 'does not make a link from a comment which contains color hex code with letters and numbers', () => { + expect( transformCommitUtils.linkToGithubIssue( 'Colors: first: `#8da47e`, second: `#f7ce76`.' ) ) + .to.equal( 'Colors: first: `#8da47e`, second: `#f7ce76`.' ); + } ); - it( 'does not make a link from a comment which does not contain the issue id', () => { - expect( transformCommitUtils.linkToGithubIssue( 'ckeditor/ckeditor5-dev#' ) ) - .to.equal( 'ckeditor/ckeditor5-dev#' ); - } ); + it( 'does not make a link from a comment which contains color hex code with letters or numbers only', () => { + expect( transformCommitUtils.linkToGithubIssue( 'Colors: first: `#000000`, second: `#ffffff`.' ) ) + .to.equal( 'Colors: first: `#000000`, second: `#ffffff`.' ); + } ); + } ); - it( 'does not make a link from a comment which contains color hex code with letters and numbers', () => { - stubs.getPackageJson.returns( { - name: 'test-package', - repository: 'https://github.com/ckeditor/ckeditor5-dev' - } ); + describe( 'getCommitType()', () => { + it( 'throws an error when passed unsupported commit type', () => { + expect( () => transformCommitUtils.getCommitType( 'invalid' ) ) + .to.throw( Error, 'Given invalid type of commit ("invalid").' ); + } ); - expect( transformCommitUtils.linkToGithubIssue( 'Colors: first: `#8da47e`, second: `#f7ce76`.' ) ) - .to.equal( 'Colors: first: `#8da47e`, second: `#f7ce76`.' ); - } ); + it( 'changes a singular type of commit to plural', () => { + expect( transformCommitUtils.getCommitType( 'Feature' ) ).to.equal( 'Features' ); + expect( transformCommitUtils.getCommitType( 'Fix' ) ).to.equal( 'Bug fixes' ); + expect( transformCommitUtils.getCommitType( 'Other' ) ).to.equal( 'Other changes' ); + } ); + } ); - it( 'does not make a link from a comment which contains color hex code with letters or numbers only', () => { - stubs.getPackageJson.returns( { - name: 'test-package', - repository: 'https://github.com/ckeditor/ckeditor5-dev' - } ); + describe( 'truncate()', () => { + it( 'does not modify too short sentence', () => { + const sentence = 'This is a short sentence.'; - expect( transformCommitUtils.linkToGithubIssue( 'Colors: first: `#000000`, second: `#ffffff`.' ) ) - .to.equal( 'Colors: first: `#000000`, second: `#ffffff`.' ); - } ); + expect( transformCommitUtils.truncate( sentence, 25 ) ).to.equal( sentence ); } ); - describe( 'getCommitType()', () => { - it( 'throws an error when passed unsupported commit type', () => { - expect( () => transformCommitUtils.getCommitType( 'invalid' ) ) - .to.throw( Error, 'Given invalid type of commit ("invalid").' ); - } ); + it( 'truncates too long sentence', () => { + const sentence = 'This is a short sentence.'; - it( 'changes a singular type of commit to plural', () => { - expect( transformCommitUtils.getCommitType( 'Feature' ) ).to.equal( 'Features' ); - expect( transformCommitUtils.getCommitType( 'Fix' ) ).to.equal( 'Bug fixes' ); - expect( transformCommitUtils.getCommitType( 'Other' ) ).to.equal( 'Other changes' ); - } ); + expect( transformCommitUtils.truncate( sentence, 13 ) ).to.equal( 'This is a...' ); } ); + } ); - describe( 'truncate()', () => { - it( 'does not modify too short sentence', () => { - const sentence = 'This is a short sentence.'; - - expect( transformCommitUtils.truncate( sentence, 25 ) ).to.equal( sentence ); + describe( 'getRepositoryUrl()', () => { + it( 'throws an error if package.json does not contain the "repository" property', () => { + vi.mocked( getPackageJson ).mockReturnValue( { + name: 'test-package' } ); - it( 'truncates too long sentence', () => { - const sentence = 'This is a short sentence.'; - - expect( transformCommitUtils.truncate( sentence, 13 ) ).to.equal( 'This is a...' ); - } ); + expect( () => transformCommitUtils.getRepositoryUrl() ) + .to.throw( Error, 'The package.json for "test-package" must contain the "repository" property.' ); } ); - describe( 'getRepositoryUrl()', () => { - it( 'throws an error if package.json does not contain the "repository" property', () => { - stubs.getPackageJson.returns( { - name: 'test-package' - } ); - - expect( () => transformCommitUtils.getRepositoryUrl() ) - .to.throw( Error, 'The package.json for "test-package" must contain the "repository" property.' ); + it( 'passes specified `cwd` to `getPackageJson()` util', () => { + vi.mocked( getPackageJson ).mockReturnValue( { + name: 'test-package', + repository: 'https://github.com/ckeditor/ckeditor5-dev/issues' } ); - it( 'passes specified `cwd` to `getPackageJson()` util', () => { - stubs.getPackageJson.returns( { - name: 'test-package', - repository: 'https://github.com/ckeditor/ckeditor5-dev/issues' - } ); + transformCommitUtils.getRepositoryUrl( 'foo' ); - transformCommitUtils.getRepositoryUrl( 'foo' ); + expect( getPackageJson ).toHaveBeenCalledTimes( 1 ); + expect( getPackageJson ).toHaveBeenCalledWith( 'foo' ); + } ); - expect( stubs.getPackageJson.calledOnce ).to.equal( true ); - expect( stubs.getPackageJson.firstCall.args[ 0 ] ).to.equal( 'foo' ); + it( 'returns the repository URL (packageJson.repository as a string, contains "/issues")', () => { + vi.mocked( getPackageJson ).mockReturnValue( { + name: 'test-package', + repository: 'https://github.com/ckeditor/ckeditor5-dev/issues' } ); - it( 'returns the repository URL (packageJson.repository as a string, contains "/issues")', () => { - stubs.getPackageJson.returns( { - name: 'test-package', - repository: 'https://github.com/ckeditor/ckeditor5-dev/issues' - } ); + expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + } ); - expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + it( 'returns the repository URL (packageJson.repository as a string, ends with ".git")', () => { + vi.mocked( getPackageJson ).mockReturnValue( { + name: 'test-package', + repository: 'https://github.com/ckeditor/ckeditor5-dev.git' } ); - it( 'returns the repository URL (packageJson.repository as a string, ends with ".git")', () => { - stubs.getPackageJson.returns( { - name: 'test-package', - repository: 'https://github.com/ckeditor/ckeditor5-dev.git' - } ); + expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + } ); - expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + it( 'returns the repository URL (packageJson.repository as an object)', () => { + vi.mocked( getPackageJson ).mockReturnValue( { + name: 'test-package', + repository: { + url: 'https://github.com/ckeditor/ckeditor5-dev' + } } ); - it( 'returns the repository URL (packageJson.repository as an object)', () => { - stubs.getPackageJson.returns( { - name: 'test-package', - repository: { - url: 'https://github.com/ckeditor/ckeditor5-dev' - } - } ); + expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + } ); - expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + it( 'returns the repository URL (packageJson.repository as an object, contains "/issues")', () => { + vi.mocked( getPackageJson ).mockReturnValue( { + name: 'test-package', + repository: { + url: 'https://github.com/ckeditor/ckeditor5-dev/issues', + type: 'git' + } } ); - it( 'returns the repository URL (packageJson.repository as an object, contains "/issues")', () => { - stubs.getPackageJson.returns( { - name: 'test-package', - repository: { - url: 'https://github.com/ckeditor/ckeditor5-dev/issues', - type: 'git' - } - } ); + expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + } ); - expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); + it( 'returns the repository URL (packageJson.repository as an object, ends with ".git")', () => { + vi.mocked( getPackageJson ).mockReturnValue( { + name: 'test-package', + repository: { + url: 'https://github.com/ckeditor/ckeditor5-dev.git', + type: 'git' + } } ); - it( 'returns the repository URL (packageJson.repository as an object, ends with ".git")', () => { - stubs.getPackageJson.returns( { - name: 'test-package', - repository: { - url: 'https://github.com/ckeditor/ckeditor5-dev.git', - type: 'git' - } - } ); - - expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); - } ); + expect( transformCommitUtils.getRepositoryUrl() ).to.equal( 'https://github.com/ckeditor/ckeditor5-dev' ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/truncatechangelog.js b/packages/ckeditor5-dev-release-tools/tests/utils/truncatechangelog.js new file mode 100644 index 000000000..ae26167fb --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/truncatechangelog.js @@ -0,0 +1,139 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getRepositoryUrl } from '../../lib/utils/transformcommitutils.js'; +import saveChangelog from '../../lib/utils/savechangelog.js'; +import getChangelog from '../../lib/utils/getchangelog.js'; +import truncateChangelog from '../../lib/utils/truncatechangelog.js'; +import { CHANGELOG_HEADER } from '../../lib/utils/constants.js'; + +vi.mock( '../../lib/utils/transformcommitutils.js' ); +vi.mock( '../../lib/utils/savechangelog.js' ); +vi.mock( '../../lib/utils/getchangelog.js' ); +vi.mock( '../../lib/utils/constants.js', () => ( { + CHANGELOG_HEADER: '# Changelog\n\n' +} ) ); + +describe( 'truncateChangelog()', () => { + beforeEach( () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/home/ckeditor' ); + vi.mocked( getRepositoryUrl ).mockReturnValue( 'https://github.com/ckeditor/ckeditor5-dev' ); + } ); + + it( 'does nothing if there is no changelog', () => { + vi.mocked( getChangelog ).mockReturnValue( null ); + truncateChangelog( 5 ); + expect( vi.mocked( saveChangelog ) ).not.toHaveBeenCalled(); + } ); + + it( 'truncates the changelog and adds the link to the release page (using default cwd)', () => { + const expectedChangelogEntries = [ + '## [0.3.0](https://github.com) (2017-01-13)', + '', + '3', + '', + 'Some text ## [like a release header]', + '', + '## [0.2.0](https://github.com) (2017-01-13)', + '', + '2' + ].join( '\n' ); + + const expectedChangelogFooter = [ + '', + '', + '---', + '', + 'To see all releases, visit the [release page](https://github.com/ckeditor/ckeditor5-dev/releases).', + '' + ].join( '\n' ); + + const changelogEntries = [ + expectedChangelogEntries, + '', + '## [0.1.0](https://github.com) (2017-01-13)', + '', + '1' + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelogEntries ); + + truncateChangelog( 2 ); + + expect( vi.mocked( getChangelog ) ).toHaveBeenCalledExactlyOnceWith( '/home/ckeditor' ); + expect( vi.mocked( saveChangelog ) ).toHaveBeenCalledExactlyOnceWith( + CHANGELOG_HEADER + expectedChangelogEntries + expectedChangelogFooter, + '/home/ckeditor' + ); + } ); + + it( 'truncates the changelog and adds the link to the release page (using a custom cwd)', () => { + const expectedChangelogEntries = [ + '## [0.3.0](https://github.com) (2017-01-13)', + '', + '3', + '', + 'Some text ## [like a release header]', + '', + '## [0.2.0](https://github.com) (2017-01-13)', + '', + '2' + ].join( '\n' ); + + const expectedChangelogFooter = [ + '', + '', + '---', + '', + 'To see all releases, visit the [release page](https://github.com/ckeditor/ckeditor5-dev/releases).', + '' + ].join( '\n' ); + + const changelogEntries = [ + expectedChangelogEntries, + '', + '## [0.1.0](https://github.com) (2017-01-13)', + '', + '1' + ].join( '\n' ); + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelogEntries ); + + truncateChangelog( 2, '/custom/cwd' ); + + expect( vi.mocked( getChangelog ) ).toHaveBeenCalledExactlyOnceWith( '/custom/cwd' ); + expect( vi.mocked( saveChangelog ) ).toHaveBeenCalledExactlyOnceWith( + CHANGELOG_HEADER + expectedChangelogEntries + expectedChangelogFooter, + '/custom/cwd' + ); + } ); + + it( 'does not add the link to the release page if changelog is not truncated', () => { + const expectedChangelogEntries = [ + '## [0.3.0](https://github.com) (2017-01-13)', + '', + '3', + '', + 'Some text ## [like a release header]', + '', + '## [0.2.0](https://github.com) (2017-01-13)', + '', + '2' + ].join( '\n' ); + + const expectedChangelogFooter = '\n'; + const changelogEntries = expectedChangelogEntries; + + vi.mocked( getChangelog ).mockReturnValue( CHANGELOG_HEADER + changelogEntries ); + + truncateChangelog( 2 ); + + expect( vi.mocked( saveChangelog ) ).toHaveBeenCalledExactlyOnceWith( + CHANGELOG_HEADER + expectedChangelogEntries + expectedChangelogFooter, + expect.any( String ) + ); + } ); +} ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/validaterepositorytorelease.js b/packages/ckeditor5-dev-release-tools/tests/utils/validaterepositorytorelease.js index 5e1a82811..88725ca1d 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/validaterepositorytorelease.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/validaterepositorytorelease.js @@ -3,152 +3,122 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect, vi } from 'vitest'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); +import validateRepositoryToRelease from '../../lib/utils/validaterepositorytorelease.js'; -describe( 'dev-release-tools/utils', () => { - describe( 'validateRepositoryToRelease()', () => { - let validateRepositoryToRelease, sandbox, stubs; +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); - beforeEach( () => { - sandbox = sinon.createSandbox(); +describe( 'validateRepositoryToRelease()', () => { + it( 'resolves an empty array if validation passes (remote branch exists)', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## master...origin/master' ); - stubs = { - devUtils: { - tools: { - shExec: sandbox.stub() - } - } - }; + const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', stubs.devUtils ); - - validateRepositoryToRelease = require( '../../lib/utils/validaterepositorytorelease' ); - } ); - - afterEach( () => { - sandbox.restore(); - mockery.disable(); - } ); - - it( 'resolves an empty array if validation passes (remote branch exists)', async () => { - stubs.devUtils.tools.shExec.resolves( '## master...origin/master' ); + expect( errors ).to.be.an( 'Array' ); + expect( errors.length ).to.equal( 0 ); + } ); - const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); + it( 'resolves an empty array if validation passes (missing remote branch)', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## master' ); - expect( errors ).to.be.an( 'Array' ); - expect( errors.length ).to.equal( 0 ); - } ); + const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); - it( 'resolves an empty array if validation passes (missing remote branch)', async () => { - stubs.devUtils.tools.shExec.resolves( '## master' ); + expect( errors ).to.be.an( 'Array' ); + expect( errors.length ).to.equal( 0 ); + } ); - const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); + it( 'resolves an array with errors if the release changes are not defined', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## master...origin/master' ); - expect( errors ).to.be.an( 'Array' ); - expect( errors.length ).to.equal( 0 ); - } ); + const errors = await validateRepositoryToRelease( { changes: null, version: '1.0.0' } ); - it( 'resolves an array with errors if the release changes are not defined', async () => { - stubs.devUtils.tools.shExec.resolves( '## master...origin/master' ); + expect( errors.length ).to.equal( 1 ); + expect( errors[ 0 ] ).to.equal( 'Cannot find changelog entries for version "1.0.0".' ); + } ); - const errors = await validateRepositoryToRelease( { changes: null, version: '1.0.0' } ); + it( 'resolves an array with errors if the specified version is not a string', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## master...origin/master' ); - expect( errors.length ).to.equal( 1 ); - expect( errors[ 0 ] ).to.equal( 'Cannot find changelog entries for version "1.0.0".' ); - } ); + const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: null } ); - it( 'resolves an array with errors if the specified version is not a string', async () => { - stubs.devUtils.tools.shExec.resolves( '## master...origin/master' ); + expect( errors.length ).to.equal( 1 ); + expect( errors[ 0 ] ).to.equal( 'Passed an invalid version ("null").' ); + } ); - const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: null } ); + it( 'resolves an array with errors if the specified version is empty string', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## master...origin/master' ); - expect( errors.length ).to.equal( 1 ); - expect( errors[ 0 ] ).to.equal( 'Passed an invalid version ("null").' ); - } ); + const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '' } ); - it( 'resolves an array with errors if the specified version is empty string', async () => { - stubs.devUtils.tools.shExec.resolves( '## master...origin/master' ); + expect( errors.length ).to.equal( 1 ); + expect( errors[ 0 ] ).to.equal( 'Passed an invalid version ("").' ); + } ); - const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '' } ); + it( 'resolves an array with errors if current branch is not "master" (remote branch exists)', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## develop...origin/develop' ); - expect( errors.length ).to.equal( 1 ); - expect( errors[ 0 ] ).to.equal( 'Passed an invalid version ("").' ); - } ); + const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); - it( 'resolves an array with errors if current branch is not "master" (remote branch exists)', async () => { - stubs.devUtils.tools.shExec.resolves( '## develop...origin/develop' ); + expect( errors.length ).to.equal( 1 ); + expect( errors[ 0 ] ).to.equal( 'Not on the "#master" branch.' ); + } ); - const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); + it( 'resolves an array with errors if current branch is not "master" (missing remote branch)', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## develop' ); - expect( errors.length ).to.equal( 1 ); - expect( errors[ 0 ] ).to.equal( 'Not on the "#master" branch.' ); - } ); + const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); - it( 'resolves an array with errors if current branch is not "master" (missing remote branch)', async () => { - stubs.devUtils.tools.shExec.resolves( '## develop' ); + expect( errors.length ).to.equal( 1 ); + expect( errors[ 0 ] ).to.equal( 'Not on the "#master" branch.' ); + } ); - const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); + it( 'resolves an array with errors if master is behind with origin (remote branch exists)', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## master...origin/master [behind 2]' ); - expect( errors.length ).to.equal( 1 ); - expect( errors[ 0 ] ).to.equal( 'Not on the "#master" branch.' ); - } ); + const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); - it( 'resolves an array with errors if master is behind with origin (remote branch exists)', async () => { - stubs.devUtils.tools.shExec.resolves( '## master...origin/master [behind 2]' ); + expect( errors.length ).to.equal( 1 ); + expect( errors[ 0 ] ).to.equal( 'The branch is behind with the remote.' ); + } ); - const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); + it( 'resolves an array with errors if master is behind with origin (missing remote branch)', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## master [behind 2]' ); - expect( errors.length ).to.equal( 1 ); - expect( errors[ 0 ] ).to.equal( 'The branch is behind with the remote.' ); - } ); + const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); - it( 'resolves an array with errors if master is behind with origin (missing remote branch)', async () => { - stubs.devUtils.tools.shExec.resolves( '## master [behind 2]' ); + expect( errors.length ).to.equal( 1 ); + expect( errors[ 0 ] ).to.equal( 'The branch is behind with the remote.' ); + } ); - const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0' } ); + it( 'allows skipping the branch check', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## develop...origin/develop' ); - expect( errors.length ).to.equal( 1 ); - expect( errors[ 0 ] ).to.equal( 'The branch is behind with the remote.' ); - } ); + const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0', ignoreBranchCheck: true } ); - it( 'allows skipping the branch check', async () => { - stubs.devUtils.tools.shExec.resolves( '## develop...origin/develop' ); + expect( errors.length ).to.equal( 0 ); + } ); - const errors = await validateRepositoryToRelease( { changes: 'Some changes.', version: '1.0.0', ignoreBranchCheck: true } ); + it( 'uses non-master branch for releasing if specified', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## release...origin/release' ); - expect( errors.length ).to.equal( 0 ); - } ); + const errors = await validateRepositoryToRelease( { branch: 'release', changes: 'Some changes.', version: '1.0.0' } ); - it( 'uses non-master branch for releasing if specified', async () => { - stubs.devUtils.tools.shExec.resolves( '## release...origin/release' ); + expect( errors ).to.be.an( 'Array' ); + expect( errors.length ).to.equal( 0 ); + } ); - const errors = await validateRepositoryToRelease( { branch: 'release', changes: 'Some changes.', version: '1.0.0' } ); + it( 'allows skipping the branch check (even if specified)', async () => { + vi.mocked( tools.shExec ).mockResolvedValue( '## develop...origin/develop' ); - expect( errors ).to.be.an( 'Array' ); - expect( errors.length ).to.equal( 0 ); + const errors = await validateRepositoryToRelease( { + branch: 'release', + changes: 'Some changes.', + version: '1.0.0', + ignoreBranchCheck: true } ); - it( 'allows skipping the branch check (even if specified)', async () => { - stubs.devUtils.tools.shExec.resolves( '## develop...origin/develop' ); - - const errors = await validateRepositoryToRelease( { - branch: 'release', - changes: 'Some changes.', - version: '1.0.0', - ignoreBranchCheck: true - } ); - - expect( errors.length ).to.equal( 0 ); - } ); + expect( errors.length ).to.equal( 0 ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/versions.js b/packages/ckeditor5-dev-release-tools/tests/utils/versions.js index 9bf982b01..c9a47a280 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/versions.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/versions.js @@ -3,342 +3,339 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import getChangelog from '../../lib/utils/getchangelog.js'; +import getPackageJson from '../../lib/utils/getpackagejson.js'; + +import { + getLastFromChangelog, + getLastPreRelease, + getLastNightly, + getNextPreRelease, + getNextNightly, + getLastTagFromGit, + getCurrent +} from '../../lib/utils/versions.js'; + +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( '../../lib/utils/getchangelog.js' ); +vi.mock( '../../lib/utils/getpackagejson.js' ); + +describe( 'versions', () => { + describe( 'getLastFromChangelog()', () => { + it( 'returns null if the changelog is invalid', () => { + vi.mocked( getChangelog ).mockReturnValue( 'Example changelog.' ); + + expect( getLastFromChangelog() ).to.equal( null ); + } ); -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); + it( 'returns version from changelog #1', () => { + vi.mocked( getChangelog ).mockReturnValue( '\n## [1.0.0](...) (2017-04-05)\nSome changelog entry.\n\n## 0.0.1' ); -describe( 'dev-release-tools/utils', () => { - let version, sandbox, changelogStub, getPackageJsonStub; + expect( getLastFromChangelog() ).to.equal( '1.0.0' ); + } ); - describe( 'versions', () => { - beforeEach( () => { - sandbox = sinon.createSandbox(); + it( 'returns version from changelog #2', () => { + vi.mocked( getChangelog ).mockReturnValue( '\n## 1.0.0 (2017-04-05)\nSome changelog entry.' ); + + expect( getLastFromChangelog() ).to.equal( '1.0.0' ); + } ); - changelogStub = sandbox.stub(); - getPackageJsonStub = sandbox.stub(); + it( 'returns version from changelog #3', () => { + vi.mocked( getChangelog ).mockReturnValue( '\n## [1.0.0-alpha](...) (2017-04-05)\nSome changelog entry.\n\n## 0.0.1' ); - version = proxyquire( '../../lib/utils/versions', { - '@ckeditor/ckeditor5-dev-utils': { - tools - }, - './getpackagejson': getPackageJsonStub, - './changelog': { - getChangelog: changelogStub - } - } ); + expect( getLastFromChangelog() ).to.equal( '1.0.0-alpha' ); } ); - afterEach( () => { - sandbox.restore(); + it( 'returns version from changelog #4', () => { + vi.mocked( getChangelog ).mockReturnValue( '\n## 1.0.0-alpha (2017-04-05)\nSome changelog entry.' ); + + expect( getLastFromChangelog() ).to.equal( '1.0.0-alpha' ); + } ); + + it( 'returns version from changelog #5', () => { + vi.mocked( getChangelog ).mockReturnValue( '\n## [1.0.0-alpha+001](...) (2017-04-05)\nSome changelog entry.\n\n## 0.0.1' ); + + expect( getLastFromChangelog() ).to.equal( '1.0.0-alpha+001' ); + } ); + + it( 'returns version from changelog #6', () => { + vi.mocked( getChangelog ).mockReturnValue( '\n## 1.0.0-alpha+001 (2017-04-05)\nSome changelog entry.' ); + + expect( getLastFromChangelog() ).to.equal( '1.0.0-alpha+001' ); + } ); + + it( 'returns version from changelog #7', () => { + vi.mocked( getChangelog ).mockReturnValue( '\n## [1.0.0-beta.2](...) (2017-04-05)\nSome changelog entry.\n\n## 0.0.1' ); + + expect( getLastFromChangelog() ).to.equal( '1.0.0-beta.2' ); + } ); + + it( 'returns version from changelog #8', () => { + vi.mocked( getChangelog ).mockReturnValue( '\n## 1.0.0-beta.2 (2017-04-05)\nSome changelog entry.' ); + + expect( getLastFromChangelog() ).to.equal( '1.0.0-beta.2' ); } ); - describe( 'getLastFromChangelog()', () => { - it( 'returns null if the changelog is invalid', () => { - changelogStub.returns( 'Example changelog.' ); + it( 'returns version from changelog #9', () => { + vi.mocked( getChangelog ).mockReturnValue( '\n## 1.0.0\nSome changelog entry.' ); - expect( version.getLastFromChangelog() ).to.equal( null ); - } ); + expect( getLastFromChangelog() ).to.equal( '1.0.0' ); + } ); - it( 'returns version from changelog #1', () => { - changelogStub.returns( '\n## [1.0.0](...) (2017-04-05)\nSome changelog entry.\n\n## 0.0.1' ); + it( 'returns null for empty changelog', () => { + vi.mocked( getChangelog ).mockReturnValue( '' ); - expect( version.getLastFromChangelog() ).to.equal( '1.0.0' ); - } ); + expect( getLastFromChangelog() ).to.equal( null ); + } ); - it( 'returns version from changelog #2', () => { - changelogStub.returns( '\n## 1.0.0 (2017-04-05)\nSome changelog entry.' ); + it( 'returns null if changelog does not exist', () => { + vi.mocked( getChangelog ).mockReturnValue( null ); - expect( version.getLastFromChangelog() ).to.equal( '1.0.0' ); - } ); + expect( getLastFromChangelog() ).to.equal( null ); + } ); + } ); + + describe( 'getLastPreRelease()', () => { + beforeEach( () => { + vi.mocked( getPackageJson ).mockReturnValue( { name: 'ckeditor5' } ); + } ); + + it( 'asks npm for all versions of a package', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [] ) ); + + return getLastPreRelease( '42.0.0-alpha' ) + .then( () => { + expect( tools.shExec ).toHaveBeenCalledTimes( 1 ); + expect( tools.shExec ).toHaveBeenCalledWith( 'npm view ckeditor5 versions --json', expect.anything() ); + } ); + } ); + + it( 'returns null if there is no version for a package', () => { + vi.mocked( tools.shExec ).mockRejectedValue(); + + return getLastPreRelease( '42.0.0-alpha' ) + .then( result => { + expect( result ).to.equal( null ); + } ); + } ); - it( 'returns version from changelog #3', () => { - changelogStub.returns( '\n## [1.0.0-alpha](...) (2017-04-05)\nSome changelog entry.\n\n## 0.0.1' ); + it( 'returns null if there is no pre-release version matching the release identifier', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230615.0', + '37.0.0-alpha.0', + '37.0.0-alpha.1', + '41.0.0', + '42.0.0' + ] ) ); + + return getLastPreRelease( '42.0.0-alpha' ) + .then( result => { + expect( result ).to.equal( null ); + } ); + } ); - expect( version.getLastFromChangelog() ).to.equal( '1.0.0-alpha' ); - } ); + it( 'returns last pre-release version matching the release identifier', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230615.0', + '37.0.0-alpha.0', + '37.0.0-alpha.1', + '41.0.0', + '42.0.0' + ] ) ); + + return getLastPreRelease( '37.0.0-alpha' ) + .then( result => { + expect( result ).to.equal( '37.0.0-alpha.1' ); + } ); + } ); - it( 'returns version from changelog #4', () => { - changelogStub.returns( '\n## 1.0.0-alpha (2017-04-05)\nSome changelog entry.' ); + it( 'returns last pre-release version matching the release identifier (non-chronological versions order)', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230615.0', + '37.0.0-alpha.0', + '37.0.0-alpha.2', + '41.0.0', + '42.0.0', + '37.0.0-alpha.1' + ] ) ); + + return getLastPreRelease( '37.0.0-alpha' ) + .then( result => { + expect( result ).to.equal( '37.0.0-alpha.2' ); + } ); + } ); - expect( version.getLastFromChangelog() ).to.equal( '1.0.0-alpha' ); - } ); + it( 'returns last pre-release version matching the release identifier (sequence numbers greater than 10)', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230615.0', + '37.0.0-alpha.1', + '37.0.0-alpha.2', + '37.0.0-alpha.3', + '41.0.0', + '37.0.0-alpha.10', + '37.0.0-alpha.11' + ] ) ); + + return getLastPreRelease( '37.0.0-alpha' ) + .then( result => { + expect( result ).to.equal( '37.0.0-alpha.11' ); + } ); + } ); - it( 'returns version from changelog #5', () => { - changelogStub.returns( '\n## [1.0.0-alpha+001](...) (2017-04-05)\nSome changelog entry.\n\n## 0.0.1' ); + it( 'returns last nightly version', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230614.0', + '0.0.0-nightly-20230615.0', + '0.0.0-nightly-20230615.1', + '0.0.0-nightly-20230615.2', + '0.0.0-nightly-20230616.0', + '37.0.0-alpha.0', + '37.0.0-alpha.2', + '41.0.0', + '42.0.0' + ] ) ); + + return getLastPreRelease( '0.0.0-nightly' ) + .then( result => { + expect( result ).to.equal( '0.0.0-nightly-20230616.0' ); + } ); + } ); - expect( version.getLastFromChangelog() ).to.equal( '1.0.0-alpha+001' ); - } ); + it( 'returns last nightly version from a specified day', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230614.0', + '0.0.0-nightly-20230615.0', + '0.0.0-nightly-20230615.1', + '0.0.0-nightly-20230615.2', + '0.0.0-nightly-20230616.0', + '37.0.0-alpha.0', + '37.0.0-alpha.2', + '41.0.0', + '42.0.0' + ] ) ); + + return getLastPreRelease( '0.0.0-nightly-20230615' ) + .then( result => { + expect( result ).to.equal( '0.0.0-nightly-20230615.2' ); + } ); + } ); + } ); - it( 'returns version from changelog #6', () => { - changelogStub.returns( '\n## 1.0.0-alpha+001 (2017-04-05)\nSome changelog entry.' ); + describe( 'getLastNightly()', () => { + beforeEach( async () => { + vi.mocked( getPackageJson ).mockReturnValue( { name: 'ckeditor5' } ); + } ); - expect( version.getLastFromChangelog() ).to.equal( '1.0.0-alpha+001' ); - } ); + it( 'returns last nightly pre-release version', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230613.0', + '0.0.0-nightly-20230614.0', + '0.0.0-nightly-20230614.1', + '0.0.0-nightly-20230614.2', + '0.0.0-nightly-20230615.0', + '37.0.0-alpha.0', + '42.0.0' + ] ) ); + + return getLastNightly() + .then( result => { + expect( result ).to.equal( '0.0.0-nightly-20230615.0' ); + } ); + } ); + } ); - it( 'returns version from changelog #7', () => { - changelogStub.returns( '\n## [1.0.0-beta.2](...) (2017-04-05)\nSome changelog entry.\n\n## 0.0.1' ); + describe( 'getNextPreRelease()', () => { + beforeEach( async () => { + vi.mocked( getPackageJson ).mockReturnValue( { name: 'ckeditor5' } ); + } ); - expect( version.getLastFromChangelog() ).to.equal( '1.0.0-beta.2' ); - } ); + it( 'returns pre-release version with id = 0 if pre-release version was never published for the package yet', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230615.0', + '37.0.0-alpha.0', + '42.0.0' + ] ) ); - it( 'returns version from changelog #8', () => { - changelogStub.returns( '\n## 1.0.0-beta.2 (2017-04-05)\nSome changelog entry.' ); + return getNextPreRelease( '42.0.0-alpha' ) + .then( result => { + expect( result ).to.equal( '42.0.0-alpha.0' ); + } ); + } ); - expect( version.getLastFromChangelog() ).to.equal( '1.0.0-beta.2' ); - } ); + it( 'returns pre-release version with incremented id if older pre-release version was already published', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230615.0', + '37.0.0-alpha.0', + '42.0.0-alpha.5' + ] ) ); - it( 'returns version from changelog #9', () => { - changelogStub.returns( '\n## 1.0.0\nSome changelog entry.' ); + return getNextPreRelease( '42.0.0-alpha' ) + .then( result => { + expect( result ).to.equal( '42.0.0-alpha.6' ); + } ); + } ); - expect( version.getLastFromChangelog() ).to.equal( '1.0.0' ); - } ); - - it( 'returns null for empty changelog', () => { - changelogStub.returns( '' ); + it( 'returns nightly version with incremented id if older nightly version was already published', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230615.5', + '37.0.0-alpha.0', + '42.0.0' + ] ) ); - expect( version.getLastFromChangelog() ).to.equal( null ); - } ); - - it( 'returns null if changelog does not exist', () => { - changelogStub.returns( null ); - - expect( version.getLastFromChangelog() ).to.equal( null ); - } ); - } ); - - describe( 'getLastPreRelease()', () => { - let shExecStub; - - beforeEach( () => { - shExecStub = sandbox.stub( tools, 'shExec' ); - getPackageJsonStub.returns( { name: 'ckeditor5' } ); - } ); - - it( 'asks npm for all versions of a package', () => { - shExecStub.resolves( JSON.stringify( [] ) ); - - return version.getLastPreRelease( '42.0.0-alpha' ) - .then( () => { - expect( shExecStub.callCount ).to.equal( 1 ); - expect( shExecStub.firstCall.args[ 0 ] ).to.equal( 'npm view ckeditor5 versions --json' ); - } ); - } ); - - it( 'returns null if there is no version for a package', () => { - shExecStub.rejects(); - - return version.getLastPreRelease( '42.0.0-alpha' ) - .then( result => { - expect( result ).to.equal( null ); - } ); - } ); - - it( 'returns null if there is no pre-release version matching the release identifier', () => { - shExecStub.resolves( JSON.stringify( [ - '0.0.0-nightly-20230615.0', - '37.0.0-alpha.0', - '37.0.0-alpha.1', - '41.0.0', - '42.0.0' - ] ) ); - - return version.getLastPreRelease( '42.0.0-alpha' ) - .then( result => { - expect( result ).to.equal( null ); - } ); - } ); - - it( 'returns last pre-release version matching the release identifier', () => { - shExecStub.resolves( JSON.stringify( [ - '0.0.0-nightly-20230615.0', - '37.0.0-alpha.0', - '37.0.0-alpha.1', - '41.0.0', - '42.0.0' - ] ) ); - - return version.getLastPreRelease( '37.0.0-alpha' ) - .then( result => { - expect( result ).to.equal( '37.0.0-alpha.1' ); - } ); - } ); - - it( 'returns last pre-release version matching the release identifier (non-chronological versions order)', () => { - shExecStub.resolves( JSON.stringify( [ - '0.0.0-nightly-20230615.0', - '37.0.0-alpha.0', - '37.0.0-alpha.2', - '41.0.0', - '42.0.0', - '37.0.0-alpha.1' - ] ) ); - - return version.getLastPreRelease( '37.0.0-alpha' ) - .then( result => { - expect( result ).to.equal( '37.0.0-alpha.2' ); - } ); - } ); - - it( 'returns last pre-release version matching the release identifier (sequence numbers greater than 10)', () => { - shExecStub.resolves( JSON.stringify( [ - '0.0.0-nightly-20230615.0', - '37.0.0-alpha.1', - '37.0.0-alpha.2', - '37.0.0-alpha.3', - '41.0.0', - '37.0.0-alpha.10', - '37.0.0-alpha.11' - ] ) ); - - return version.getLastPreRelease( '37.0.0-alpha' ) - .then( result => { - expect( result ).to.equal( '37.0.0-alpha.11' ); - } ); - } ); - - it( 'returns last nightly version', () => { - shExecStub.resolves( JSON.stringify( [ - '0.0.0-nightly-20230614.0', - '0.0.0-nightly-20230615.0', - '0.0.0-nightly-20230615.1', - '0.0.0-nightly-20230615.2', - '0.0.0-nightly-20230616.0', - '37.0.0-alpha.0', - '37.0.0-alpha.2', - '41.0.0', - '42.0.0' - ] ) ); - - return version.getLastPreRelease( '0.0.0-nightly' ) - .then( result => { - expect( result ).to.equal( '0.0.0-nightly-20230616.0' ); - } ); - } ); - - it( 'returns last nightly version from a specified day', () => { - shExecStub.resolves( JSON.stringify( [ - '0.0.0-nightly-20230614.0', - '0.0.0-nightly-20230615.0', - '0.0.0-nightly-20230615.1', - '0.0.0-nightly-20230615.2', - '0.0.0-nightly-20230616.0', - '37.0.0-alpha.0', - '37.0.0-alpha.2', - '41.0.0', - '42.0.0' - ] ) ); - - return version.getLastPreRelease( '0.0.0-nightly-20230615' ) - .then( result => { - expect( result ).to.equal( '0.0.0-nightly-20230615.2' ); - } ); - } ); - } ); - - describe( 'getLastNightly()', () => { - beforeEach( () => { - sandbox.stub( version, 'getLastPreRelease' ).resolves( '0.0.0-nightly-20230615.0' ); - } ); - - it( 'asks for a last nightly pre-release version', () => { - return version.getLastNightly() - .then( result => { - expect( version.getLastPreRelease.callCount ).to.equal( 1 ); - expect( version.getLastPreRelease.firstCall.args[ 0 ] ).to.equal( '0.0.0-nightly' ); - - expect( result ).to.equal( '0.0.0-nightly-20230615.0' ); - } ); - } ); - } ); - - describe( 'getNextPreRelease()', () => { - it( 'asks for a last pre-release version', () => { - sandbox.stub( version, 'getLastPreRelease' ).resolves( null ); - - return version.getNextPreRelease( '42.0.0-alpha' ) - .then( () => { - expect( version.getLastPreRelease.calledOnce ).to.equal( true ); - expect( version.getLastPreRelease.firstCall.args[ 0 ] ).to.equal( '42.0.0-alpha' ); - } ); - } ); - - it( 'returns pre-release version with id = 0 if pre-release version was never published for the package yet', () => { - sandbox.stub( version, 'getLastPreRelease' ).resolves( null ); - - return version.getNextPreRelease( '42.0.0-alpha' ) - .then( result => { - expect( result ).to.equal( '42.0.0-alpha.0' ); - } ); - } ); - - it( 'returns pre-release version with incremented id if older pre-release version was already published', () => { - sandbox.stub( version, 'getLastPreRelease' ).resolves( '42.0.0-alpha.5' ); - - return version.getNextPreRelease( '42.0.0-alpha' ) - .then( result => { - expect( result ).to.equal( '42.0.0-alpha.6' ); - } ); - } ); - - it( 'returns nightly version with incremented id if older nightly version was already published', () => { - sandbox.stub( version, 'getLastPreRelease' ).resolves( '0.0.0-nightly-20230615.5' ); - - return version.getNextPreRelease( '0.0.0-nightly' ) - .then( result => { - expect( result ).to.equal( '0.0.0-nightly-20230615.6' ); - } ); - } ); - } ); - - describe( 'getNextNightly()', () => { - let clock; - - beforeEach( () => { - sandbox.stub( version, 'getNextPreRelease' ).resolves( '0.0.0-nightly-20230615.1' ); - - clock = sinon.useFakeTimers( { - now: new Date( '2023-06-15 12:00:00' ) + return getNextPreRelease( '0.0.0-nightly' ) + .then( result => { + expect( result ).to.equal( '0.0.0-nightly-20230615.6' ); } ); - } ); + } ); + } ); - afterEach( () => { - clock.restore(); - } ); + describe( 'getNextNightly()', () => { + beforeEach( () => { + vi.mocked( getPackageJson ).mockReturnValue( { name: 'ckeditor5' } ); - it( 'asks for a last nightly pre-release version', () => { - return version.getNextNightly() - .then( result => { - expect( version.getNextPreRelease.calledOnce ).to.equal( true ); - expect( version.getNextPreRelease.firstCall.args[ 0 ] ).to.equal( '0.0.0-nightly-20230615' ); + vi.useFakeTimers(); + vi.setSystemTime( new Date( '2023-06-15 12:00:00' ) ); + } ); - expect( result ).to.equal( '0.0.0-nightly-20230615.1' ); - } ); - } ); + afterEach( () => { + vi.useRealTimers(); + } ); + + it( 'asks for a last nightly pre-release version', () => { + vi.mocked( tools.shExec ).mockResolvedValue( JSON.stringify( [ + '0.0.0-nightly-20230615.0', + '37.0.0-alpha.0', + '42.0.0' + ] ) ); + + return getNextNightly() + .then( result => { + expect( result ).to.equal( '0.0.0-nightly-20230615.1' ); + } ); } ); + } ); - describe( 'getLastTagFromGit()', () => { - it( 'returns last tag if exists', () => { - sandbox.stub( tools, 'shExec' ).returns( 'v1.0.0' ); + describe( 'getLastTagFromGit()', () => { + it( 'returns last tag if exists', () => { + vi.mocked( tools.shExec ).mockReturnValue( 'v1.0.0' ); - expect( version.getLastTagFromGit() ).to.equal( '1.0.0' ); - } ); + expect( getLastTagFromGit() ).to.equal( '1.0.0' ); + } ); - it( 'returns null if tags do not exist', () => { - sandbox.stub( tools, 'shExec' ).returns( '' ); + it( 'returns null if tags do not exist', () => { + vi.mocked( tools.shExec ).mockReturnValue( '' ); - expect( version.getLastTagFromGit() ).to.equal( null ); - } ); + expect( getLastTagFromGit() ).to.equal( null ); } ); + } ); - describe( 'getCurrent()', () => { - it( 'returns current version from "package.json"', () => { - getPackageJsonStub.returns( { version: '0.1.2' } ); + describe( 'getCurrent()', () => { + it( 'returns current version from "package.json"', () => { + vi.mocked( getPackageJson ).mockReturnValue( { version: '0.1.2' } ); - expect( version.getCurrent() ).to.equal( '0.1.2' ); - } ); + expect( getCurrent() ).to.equal( '0.1.2' ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/vitest.config.js b/packages/ckeditor5-dev-release-tools/vitest.config.js new file mode 100644 index 000000000..de04db7b6 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/vitest.config.js @@ -0,0 +1,54 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig( { + plugins: [ + executeInParallelVirtualModulePlugin() + ], + test: { + setupFiles: [ + './tests/_utils/testsetup.js' + ], + testTimeout: 10000, + mockReset: true, + restoreMocks: true, + include: [ + 'tests/**/*.js' + ], + exclude: [ + './tests/_utils/**/*.js', + './tests/test-fixtures/**/*.js' + ], + coverage: { + provider: 'v8', + include: [ + 'lib/**' + ], + + reporter: [ 'text', 'json', 'html', 'lcov' ] + } + } +} ); + +function executeInParallelVirtualModulePlugin() { + const virtualModuleId = 'virtual:parallelworker-integration-module'; + const resolvedVirtualModuleId = '\0' + virtualModuleId; + + return { + name: 'execute-in-parallel-virtual-module-plugin', + resolveId( id ) { + if ( id === virtualModuleId ) { + return resolvedVirtualModuleId; + } + }, + load( id ) { + if ( id === resolvedVirtualModuleId ) { + return 'export default function virtualModule() {}'; + } + } + }; +} diff --git a/packages/ckeditor5-dev-stale-bot/bin/stale-bot.js b/packages/ckeditor5-dev-stale-bot/bin/stale-bot.js index d08a2a800..fed7a0cd9 100755 --- a/packages/ckeditor5-dev-stale-bot/bin/stale-bot.js +++ b/packages/ckeditor5-dev-stale-bot/bin/stale-bot.js @@ -5,15 +5,13 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs-extra' ); -const chalk = require( 'chalk' ); -const createSpinner = require( './utils/createspinner' ); -const parseArguments = require( './utils/parsearguments' ); -const validateConfig = require( './utils/validateconfig' ); -const parseConfig = require( './utils/parseconfig' ); -const GitHubRepository = require( '../lib/githubrepository' ); +import fs from 'fs-extra'; +import chalk from 'chalk'; +import createSpinner from './utils/createspinner.js'; +import parseArguments from './utils/parsearguments.js'; +import validateConfig from './utils/validateconfig.js'; +import parseConfig from './utils/parseconfig.js'; +import GitHubRepository from '../lib/githubrepository.js'; main().catch( error => { console.error( '\n🔥 Unable to process stale issues and pull requests.\n', error ); @@ -34,7 +32,7 @@ async function main() { throw new Error( 'Missing or invalid CLI argument: --config-path' ); } - const config = require( configPath ); + const { default: config } = await import( configPath ); validateConfig( config ); @@ -210,7 +208,7 @@ async function handleActions( githubRepository, entries, actions, spinner ) { /** * Prints in the console a welcome message. * - * @param {Boolean} dryRun Indicates if dry run mode is enabled. + * @param {boolean} dryRun Indicates if dry run mode is enabled. */ function printWelcomeMessage( dryRun ) { const message = [ @@ -227,7 +225,7 @@ function printWelcomeMessage( dryRun ) { /** * Prints in the console status messages about actions required to be executed on found issues and pull requests. * - * @param {Boolean} dryRun Indicates if dry run mode is enabled. + * @param {boolean} dryRun Indicates if dry run mode is enabled. * @param {SearchResult} searchResult Found issues and pull requests that require an action. * @param {Options} options Configuration options. */ @@ -286,7 +284,7 @@ function printStatus( dryRun, searchResult, options ) { /** * Prints in the console issues and pull requests from a single section. * - * @param {String} statusMessage Seaction header. + * @param {string} statusMessage Section header. * @param {Array.} entries Found issues and pull requests. */ function printStatusSection( statusMessage, entries ) { @@ -298,7 +296,7 @@ function printStatusSection( statusMessage, entries ) { } /** - * @typedef {Object} SearchResult + * @typedef {object} SearchResult * @property {Array.} issuesOrPullRequestsToClose * @property {Array.} issuesOrPullRequestsToStale * @property {Array.} issuesOrPullRequestsToUnstale @@ -307,21 +305,21 @@ function printStatusSection( statusMessage, entries ) { */ /** - * @typedef {Object} Actions + * @typedef {object} Actions * @property {HandleActionsCommentToAdd} [commentToAdd] * @property {HandleActionsLabelsToAdd} [labelsToAdd] - * @property {Array.} [labelsToRemove] - * @property {Boolean} [close] + * @property {Array.} [labelsToRemove] + * @property {boolean} [close] */ /** * @callback HandleActionsLabelsToAdd * @param {IssueOrPullRequestResult} entry - * @returns {Array.} + * @returns {Array.} */ /** * @callback HandleActionsCommentToAdd * @param {IssueOrPullRequestResult} entry - * @returns {String} + * @returns {string} */ diff --git a/packages/ckeditor5-dev-stale-bot/bin/utils/createspinner.js b/packages/ckeditor5-dev-stale-bot/bin/utils/createspinner.js index 9c74f1b72..3886327be 100644 --- a/packages/ckeditor5-dev-stale-bot/bin/utils/createspinner.js +++ b/packages/ckeditor5-dev-stale-bot/bin/utils/createspinner.js @@ -3,17 +3,15 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const ora = require( 'ora' ); -const chalk = require( 'chalk' ); +import ora from 'ora'; +import chalk from 'chalk'; /** * Creates the spinner instance with methods to update spinner text. * * @returns {Spinner} */ -module.exports = function createSpinner() { +export default function createSpinner() { const instance = ora(); const printStatus = text => { @@ -40,10 +38,10 @@ module.exports = function createSpinner() { printStatus, onProgress }; -}; +} /** - * @typedef {Object} Spinner + * @typedef {object} Spinner * @property {ora.Ora} instance * @property {Function} printStatus * @property {Function} onProgress diff --git a/packages/ckeditor5-dev-stale-bot/bin/utils/parsearguments.js b/packages/ckeditor5-dev-stale-bot/bin/utils/parsearguments.js index ac394ea3e..c113c4829 100644 --- a/packages/ckeditor5-dev-stale-bot/bin/utils/parsearguments.js +++ b/packages/ckeditor5-dev-stale-bot/bin/utils/parsearguments.js @@ -3,20 +3,18 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const minimist = require( 'minimist' ); -const upath = require( 'upath' ); +import minimist from 'minimist'; +import upath from 'upath'; /** * Parses CLI arguments. * - * @param {Array.} args - * @returns {Object} result - * @returns {Boolean} result.dryRun - * @returns {String} result.configPath + * @param {Array.} args + * @returns {object} result + * @returns {boolean} result.dryRun + * @returns {string} result.configPath */ -module.exports = function parseArguments( args ) { +export default function parseArguments( args ) { const config = { boolean: [ 'dry-run' @@ -38,4 +36,4 @@ module.exports = function parseArguments( args ) { dryRun: options[ 'dry-run' ], configPath: upath.join( process.cwd(), options[ 'config-path' ] ) }; -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/bin/utils/parseconfig.js b/packages/ckeditor5-dev-stale-bot/bin/utils/parseconfig.js index a397e1b84..aa055a397 100644 --- a/packages/ckeditor5-dev-stale-bot/bin/utils/parseconfig.js +++ b/packages/ckeditor5-dev-stale-bot/bin/utils/parseconfig.js @@ -3,18 +3,16 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { subDays, formatISO } = require( 'date-fns' ); +import { subDays, formatISO } from 'date-fns'; /** * Converts configuration options into format required by the GitHubRepository. * - * @param {String} viewerLogin The GitHub login of the currently authenticated user. + * @param {string} viewerLogin The GitHub login of the currently authenticated user. * @param {Config} config Configuration options. * @returns {Options} */ -module.exports = function parseConfig( viewerLogin, config ) { +export default function parseConfig( viewerLogin, config ) { const { REPOSITORY_SLUG, STALE_LABELS, @@ -63,49 +61,49 @@ module.exports = function parseConfig( viewerLogin, config ) { [ ...IGNORED_ACTIVITY_LOGINS, viewerLogin ] : IGNORED_ACTIVITY_LOGINS }; -}; +} /** - * @typedef {Object} Config - * @property {String} GITHUB_TOKEN - * @property {String} REPOSITORY_SLUG - * @property {Array.} STALE_LABELS - * @property {String} STALE_ISSUE_MESSAGE - * @property {String} STALE_PR_MESSAGE - * @property {Array.} CLOSE_ISSUE_LABELS - * @property {String} CLOSE_ISSUE_MESSAGE - * @property {Array.} CLOSE_PR_LABELS - * @property {String} CLOSE_PR_MESSAGE - * @property {String} [STALE_PENDING_ISSUE_MESSAGE=STALE_ISSUE_MESSAGE] - * @property {Array.} [PENDING_ISSUE_LABELS=[]] - * @property {Number} [DAYS_BEFORE_STALE=365] - * @property {Number} [DAYS_BEFORE_STALE_PENDING_ISSUE=14] - * @property {Number} [DAYS_BEFORE_CLOSE=30] - * @property {Boolean} [IGNORE_VIEWER_ACTIVITY=true] - * @property {Array.} [IGNORED_ISSUE_LABELS=[]] - * @property {Array.} [IGNORED_PR_LABELS=[]] - * @property {Array.} [IGNORED_ACTIVITY_LABELS=[]] - * @property {Array.} [IGNORED_ACTIVITY_LOGINS=[]] + * @typedef {object} Config + * @property {string} GITHUB_TOKEN + * @property {string} REPOSITORY_SLUG + * @property {Array.} STALE_LABELS + * @property {string} STALE_ISSUE_MESSAGE + * @property {string} STALE_PR_MESSAGE + * @property {Array.} CLOSE_ISSUE_LABELS + * @property {string} CLOSE_ISSUE_MESSAGE + * @property {Array.} CLOSE_PR_LABELS + * @property {string} CLOSE_PR_MESSAGE + * @property {string} [STALE_PENDING_ISSUE_MESSAGE=STALE_ISSUE_MESSAGE] + * @property {Array.} [PENDING_ISSUE_LABELS=[]] + * @property {number} [DAYS_BEFORE_STALE=365] + * @property {number} [DAYS_BEFORE_STALE_PENDING_ISSUE=14] + * @property {number} [DAYS_BEFORE_CLOSE=30] + * @property {boolean} [IGNORE_VIEWER_ACTIVITY=true] + * @property {Array.} [IGNORED_ISSUE_LABELS=[]] + * @property {Array.} [IGNORED_PR_LABELS=[]] + * @property {Array.} [IGNORED_ACTIVITY_LABELS=[]] + * @property {Array.} [IGNORED_ACTIVITY_LOGINS=[]] */ /** - * @typedef {Object} Options - * @property {String} repositorySlug - * @property {String} staleDate - * @property {String} staleDatePendingIssue - * @property {String} closeDate - * @property {Array.} staleLabels - * @property {Boolean} shouldProcessPendingIssues - * @property {Array.} pendingIssueLabels - * @property {String} staleIssueMessage - * @property {String} stalePendingIssueMessage - * @property {String} stalePullRequestMessage - * @property {Array.} closeIssueLabels - * @property {String} closeIssueMessage - * @property {Array.} closePullRequestLabels - * @property {String} closePullRequestMessage - * @property {Array.} ignoredIssueLabels - * @property {Array.} ignoredPullRequestLabels - * @property {Array.} ignoredActivityLabels - * @property {Array.} ignoredActivityLogins + * @typedef {object} Options + * @property {string} repositorySlug + * @property {string} staleDate + * @property {string} staleDatePendingIssue + * @property {string} closeDate + * @property {Array.} staleLabels + * @property {boolean} shouldProcessPendingIssues + * @property {Array.} pendingIssueLabels + * @property {string} staleIssueMessage + * @property {string} stalePendingIssueMessage + * @property {string} stalePullRequestMessage + * @property {Array.} closeIssueLabels + * @property {string} closeIssueMessage + * @property {Array.} closePullRequestLabels + * @property {string} closePullRequestMessage + * @property {Array.} ignoredIssueLabels + * @property {Array.} ignoredPullRequestLabels + * @property {Array.} ignoredActivityLabels + * @property {Array.} ignoredActivityLogins */ diff --git a/packages/ckeditor5-dev-stale-bot/bin/utils/validateconfig.js b/packages/ckeditor5-dev-stale-bot/bin/utils/validateconfig.js index 0a5f363d5..8a4dc534c 100644 --- a/packages/ckeditor5-dev-stale-bot/bin/utils/validateconfig.js +++ b/packages/ckeditor5-dev-stale-bot/bin/utils/validateconfig.js @@ -3,8 +3,6 @@ * For licensing, see LICENSE.md. */ -'use strict'; - const requiredFields = [ 'GITHUB_TOKEN', 'REPOSITORY_SLUG', @@ -23,7 +21,7 @@ const requiredFields = [ * @param {Config} config Configuration options. * @returns {void} */ -module.exports = function validateConfig( config ) { +export default function validateConfig( config ) { const missingFields = requiredFields.filter( fieldName => !config[ fieldName ] ); if ( !missingFields.length ) { @@ -31,5 +29,5 @@ module.exports = function validateConfig( config ) { } throw new Error( `Missing configuration options: ${ missingFields.join( ', ' ) }.` ); -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/lib/githubrepository.js b/packages/ckeditor5-dev-stale-bot/lib/githubrepository.js index 9a6bfb1a4..d00404b05 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/githubrepository.js +++ b/packages/ckeditor5-dev-stale-bot/lib/githubrepository.js @@ -3,39 +3,28 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const upath = require( 'upath' ); -const fs = require( 'fs-extra' ); -const { GraphQLClient } = require( 'graphql-request' ); -const { logger } = require( '@ckeditor/ckeditor5-dev-utils' ); -const { +import upath from 'upath'; +import fs from 'fs-extra'; +import { fileURLToPath } from 'url'; +import { GraphQLClient } from 'graphql-request'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import { addSeconds, fromUnixTime, formatDistanceToNow, differenceInSeconds -} = require( 'date-fns' ); -const prepareSearchQuery = require( './utils/preparesearchquery' ); -const isIssueOrPullRequestToStale = require( './utils/isissueorpullrequesttostale' ); -const isIssueOrPullRequestToUnstale = require( './utils/isissueorpullrequesttounstale' ); -const isIssueOrPullRequestToClose = require( './utils/isissueorpullrequesttoclose' ); -const isPendingIssueToStale = require( './utils/ispendingissuetostale' ); -const isPendingIssueToUnlabel = require( './utils/ispendingissuetounlabel' ); +} from 'date-fns'; +import prepareSearchQuery from './utils/preparesearchquery.js'; +import isIssueOrPullRequestToStale from './utils/isissueorpullrequesttostale.js'; +import isIssueOrPullRequestToUnstale from './utils/isissueorpullrequesttounstale.js'; +import isIssueOrPullRequestToClose from './utils/isissueorpullrequesttoclose.js'; +import isPendingIssueToStale from './utils/ispendingissuetostale.js'; +import isPendingIssueToUnlabel from './utils/ispendingissuetounlabel.js'; -const GRAPHQL_PATH = upath.join( __dirname, 'graphql' ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = upath.dirname( __filename ); -const queries = { - getViewerLogin: readGraphQL( 'getviewerlogin' ), - searchIssuesOrPullRequests: readGraphQL( 'searchissuesorpullrequests' ), - searchPendingIssues: readGraphQL( 'searchpendingissues' ), - getIssueOrPullRequestTimelineItems: readGraphQL( 'getissueorpullrequesttimelineitems' ), - addComment: readGraphQL( 'addcomment' ), - getLabels: readGraphQL( 'getlabels' ), - addLabels: readGraphQL( 'addlabels' ), - removeLabels: readGraphQL( 'removelabels' ), - closeIssue: readGraphQL( 'closeissue' ), - closePullRequest: readGraphQL( 'closepullrequest' ) -}; +const GRAPHQL_PATH = upath.join( __dirname, 'graphql' ); /** * A GitHub client containing methods used to interact with GitHub using its GraphQL API. @@ -43,7 +32,7 @@ const queries = { * All methods handles paginated data and it supports a case when a request has exceeded the GitHub API rate limit. * In such a case, the request waits until the limit is reset and it is automatically sent again. */ -module.exports = class GitHubRepository { +export default class GitHubRepository { constructor( authToken ) { /** * @private @@ -63,15 +52,31 @@ module.exports = class GitHubRepository { * @property {Logger} */ this.logger = logger(); + + /** + * @private + */ + this.queries = { + getViewerLogin: readGraphQL( 'getviewerlogin' ), + searchIssuesOrPullRequests: readGraphQL( 'searchissuesorpullrequests' ), + searchPendingIssues: readGraphQL( 'searchpendingissues' ), + getIssueOrPullRequestTimelineItems: readGraphQL( 'getissueorpullrequesttimelineitems' ), + addComment: readGraphQL( 'addcomment' ), + getLabels: readGraphQL( 'getlabels' ), + addLabels: readGraphQL( 'addlabels' ), + removeLabels: readGraphQL( 'removelabels' ), + closeIssue: readGraphQL( 'closeissue' ), + closePullRequest: readGraphQL( 'closepullrequest' ) + }; } /** * Returns the GitHub login of the currently authenticated user. * - * @returns {Promise.} + * @returns {Promise.} */ async getViewerLogin() { - return this.sendRequest( await queries.getViewerLogin ) + return this.sendRequest( await this.queries.getViewerLogin ) .then( data => data.viewer.login ) .catch( error => { this.logger.error( 'Unexpected error when executing "#getViewerLogin()".', error ); @@ -85,7 +90,7 @@ module.exports = class GitHubRepository { * * @param {'Issue'|'PullRequest'} type Type of resource to search. * @param {Options} options Configuration options. - * @param {Function} onProgress Callback function called each time a response is received. + * @param {function} onProgress Callback function called each time a response is received. * @param {PageInfo} [pageInfo] Describes the current page of the returned result. * @returns {Promise.} */ @@ -105,7 +110,7 @@ module.exports = class GitHubRepository { cursor: pageInfo.cursor || null }; - return this.sendRequest( await queries.searchIssuesOrPullRequests, variables ) + return this.sendRequest( await this.queries.searchIssuesOrPullRequests, variables ) .then( async data => { const issuesOrPullRequests = await this.parseIssuesOrPullRequests( data.search ); @@ -135,7 +140,7 @@ module.exports = class GitHubRepository { * Searches for all stale issues and pull requests that should be closed or unstaled. * * @param {Options} options Configuration options. - * @param {Function} onProgress Callback function called each time a response is received. + * @param {function} onProgress Callback function called each time a response is received. * @param {PageInfo} [pageInfo] Describes the current page of the returned result. * @returns {Promise.} */ @@ -151,7 +156,7 @@ module.exports = class GitHubRepository { cursor: pageInfo.cursor || null }; - return this.sendRequest( await queries.searchIssuesOrPullRequests, variables ) + return this.sendRequest( await this.queries.searchIssuesOrPullRequests, variables ) .then( async data => { const issuesOrPullRequests = await this.parseIssuesOrPullRequests( data.search ); @@ -197,7 +202,7 @@ module.exports = class GitHubRepository { * Searches for all pending issues that should be staled or unlabeled. * * @param {Options} options Configuration options. - * @param {Function} onProgress Callback function called each time a response is received. + * @param {function} onProgress Callback function called each time a response is received. * @param {PageInfo} [pageInfo] Describes the current page of the returned result. * @returns {Promise.} */ @@ -217,7 +222,7 @@ module.exports = class GitHubRepository { cursor: pageInfo.cursor || null }; - return this.sendRequest( await queries.searchPendingIssues, variables ) + return this.sendRequest( await this.queries.searchPendingIssues, variables ) .then( async data => { const pendingIssues = this.parsePendingIssues( data.search ); @@ -253,7 +258,7 @@ module.exports = class GitHubRepository { /** * Fetches all timeline items for provided issue or pull request. * - * @param {String} nodeId Issue or pull request identifier for which we want to fetch timeline items. + * @param {string} nodeId Issue or pull request identifier for which we want to fetch timeline items. * @param {PageInfo} [pageInfo] Describes the current page of the returned result. * @returns {Promise.>} */ @@ -263,7 +268,7 @@ module.exports = class GitHubRepository { cursor: pageInfo.cursor || null }; - return this.sendRequest( await queries.getIssueOrPullRequestTimelineItems, variables ) + return this.sendRequest( await this.queries.getIssueOrPullRequestTimelineItems, variables ) .then( async data => { pageInfo = data.node.timelineItems.pageInfo; @@ -285,8 +290,8 @@ module.exports = class GitHubRepository { /** * Adds new comment to the specified issue or pull request on GitHub. * - * @param {String} nodeId Issue or pull request identifier for which we want to add new comment. - * @param {String} comment Comment to add. + * @param {string} nodeId Issue or pull request identifier for which we want to add new comment. + * @param {string} comment Comment to add. * @returns {Promise} */ async addComment( nodeId, comment ) { @@ -295,7 +300,7 @@ module.exports = class GitHubRepository { comment }; - return this.sendRequest( await queries.addComment, variables ) + return this.sendRequest( await this.queries.addComment, variables ) .catch( error => { this.logger.error( 'Unexpected error when executing "#addComment()".', error ); @@ -306,9 +311,9 @@ module.exports = class GitHubRepository { /** * Fetches the specified labels from GitHub. * - * @param {String} repositorySlug Identifies the repository, where the provided labels exist. - * @param {Array.} labelNames Label names to fetch. - * @returns {Promise.>} + * @param {string} repositorySlug Identifies the repository, where the provided labels exist. + * @param {Array.} labelNames Label names to fetch. + * @returns {Promise.>} */ async getLabels( repositorySlug, labelNames ) { if ( !labelNames.length ) { @@ -322,7 +327,7 @@ module.exports = class GitHubRepository { labelNames: labelNames.join( ' ' ) }; - return this.sendRequest( await queries.getLabels, variables ) + return this.sendRequest( await this.queries.getLabels, variables ) .then( data => { return data.repository.labels.nodes // Additional filtering is needed, because GitHub endpoint may return many more results than match the query. @@ -339,8 +344,8 @@ module.exports = class GitHubRepository { /** * Adds new labels to the specified issue or pull request on GitHub. * - * @param {String} nodeId Issue or pull request identifier for which we want to add labels. - * @param {Array.} labelIds Labels to add. + * @param {string} nodeId Issue or pull request identifier for which we want to add labels. + * @param {Array.} labelIds Labels to add. * @returns {Promise} */ async addLabels( nodeId, labelIds ) { @@ -349,7 +354,7 @@ module.exports = class GitHubRepository { labelIds }; - return this.sendRequest( await queries.addLabels, variables ) + return this.sendRequest( await this.queries.addLabels, variables ) .catch( error => { this.logger.error( 'Unexpected error when executing "#addLabels()".', error ); @@ -360,8 +365,8 @@ module.exports = class GitHubRepository { /** * Removes labels from the specified issue or pull request on GitHub. * - * @param {String} nodeId Issue or pull request identifier for which we want to remove labels. - * @param {Array.} labelIds Labels to remove. + * @param {string} nodeId Issue or pull request identifier for which we want to remove labels. + * @param {Array.} labelIds Labels to remove. * @returns {Promise} */ async removeLabels( nodeId, labelIds ) { @@ -370,7 +375,7 @@ module.exports = class GitHubRepository { labelIds }; - return this.sendRequest( await queries.removeLabels, variables ) + return this.sendRequest( await this.queries.removeLabels, variables ) .catch( error => { this.logger.error( 'Unexpected error when executing "#removeLabels()".', error ); @@ -382,7 +387,7 @@ module.exports = class GitHubRepository { * Closes issue or pull request. * * @param {'Issue'|'PullRequest'} type Type of resource to close. - * @param {String} nodeId Issue or pull request identifier to close. + * @param {string} nodeId Issue or pull request identifier to close. * @returns {Promise} */ async closeIssueOrPullRequest( type, nodeId ) { @@ -390,7 +395,7 @@ module.exports = class GitHubRepository { nodeId }; - const query = type === 'Issue' ? await queries.closeIssue : await queries.closePullRequest; + const query = type === 'Issue' ? await this.queries.closeIssue : await this.queries.closePullRequest; return this.sendRequest( query, variables ) .catch( error => { @@ -404,10 +409,10 @@ module.exports = class GitHubRepository { * Prepares the page pointers and search options for the next search request. * * @private - * @param {Object} data Received response to parse. + * @param {object} data Received response to parse. * @param {Options} options Configuration options. * @param {PageInfo} pageInfo Describes the current page of the returned result. - * @returns {Object} result + * @returns {object} result * @returns {PageInfo} result.nextPageInfo * @returns {Options} result.nextOptions */ @@ -452,7 +457,7 @@ module.exports = class GitHubRepository { * initial request. * * @private - * @param {Object} data Received response to parse. + * @param {object} data Received response to parse. * @returns {Promise.>} */ parseIssuesOrPullRequests( data ) { @@ -479,7 +484,7 @@ module.exports = class GitHubRepository { * Parses the received array of timeline items for an issue or pull request. * * @private - * @param {Object} data Received response to parse. + * @param {object} data Received response to parse. * @returns {Array.} */ parseIssueOrPullRequestTimelineItems( data ) { @@ -509,7 +514,7 @@ module.exports = class GitHubRepository { * initial request. * * @private - * @param {Object} data Received response to parse. + * @param {object} data Received response to parse. * @returns {Array.} */ parsePendingIssues( data ) { @@ -532,9 +537,9 @@ module.exports = class GitHubRepository { * Then, the request is sent again. * * @private - * @param {String} query The GraphQL query to send. - * @param {Object} [variables={}] Variables required by the GraphQL query. - * @returns {Promise.} + * @param {string} query The GraphQL query to send. + * @param {object} [variables={}] Variables required by the GraphQL query. + * @returns {Promise.} */ async sendRequest( query, variables = {} ) { return this.graphql.request( query, variables ) @@ -556,13 +561,13 @@ module.exports = class GitHubRepository { return Promise.reject( error ); } ); } -}; +} /** * Reads the GraphQL query from filesystem. * - * @param {String} queryName Filename of the GraphQL query to read. - * @returns {Promise.} + * @param {string} queryName Filename of the GraphQL query to read. + * @returns {Promise.} */ function readGraphQL( queryName ) { return fs.readFile( upath.join( GRAPHQL_PATH, `${ queryName }.graphql` ), 'utf-8' ); @@ -572,7 +577,7 @@ function readGraphQL( queryName ) { * Parses the received error from GitHub API and checks if it concerns exceeding the API rate limit. If yes, it returns information when the * rate limit will be reset. * - * @param {Object} error An error that was received from the GitHub API. + * @param {object} error An error that was received from the GitHub API. * @returns {RateLimitExceeded} */ function checkApiRateLimit( error ) { @@ -627,69 +632,69 @@ function mapNodeToResult( node ) { } /** - * @typedef {Object} TimelineItem - * @property {String} eventDate - * @property {String} [author] - * @property {String} [label] + * @typedef {object} TimelineItem + * @property {string} eventDate + * @property {string} [author] + * @property {string} [label] */ /** - * @typedef {Object} Comment - * @property {String} createdAt - * @property {Boolean} isExternal + * @typedef {object} Comment + * @property {string} createdAt + * @property {boolean} isExternal */ /** - * @typedef {Object} IssueOrPullRequest - * @property {String} id + * @typedef {object} IssueOrPullRequest + * @property {string} id * @property {'Issue'|'PullRequest'} type - * @property {Number} number - * @property {String} title - * @property {String} url - * @property {String} createdAt - * @property {String|null} lastEditedAt - * @property {String|null} lastReactedAt + * @property {number} number + * @property {string} title + * @property {string} url + * @property {string} createdAt + * @property {string|null} lastEditedAt + * @property {string|null} lastReactedAt * @property {Array.} timelineItems */ /** - * @typedef {Object} PendingIssue - * @property {String} id + * @typedef {object} PendingIssue + * @property {string} id * @property {'Issue'} type - * @property {String} title - * @property {String} url - * @property {Array.} labels + * @property {string} title + * @property {string} url + * @property {Array.} labels * @property {Comment|null} lastComment */ /** - * @typedef {Object} IssueOrPullRequestResult - * @property {String} id + * @typedef {object} IssueOrPullRequestResult + * @property {string} id * @property {'Issue'|'PullRequest'} type - * @property {String} url - * @property {String} title + * @property {string} url + * @property {string} title */ /** - * @typedef {Object} PageInfo - * @property {Boolean} [hasNextPage] - * @property {String} [cursor] - * @property {Number} [done] - * @property {Number} [total] + * @typedef {object} PageInfo + * @property {boolean} [hasNextPage] + * @property {string} [cursor] + * @property {number} [done] + * @property {number} [total] */ /** - * @typedef {Object} Logger + * @typedef {object} Logger * @property {Function} info * @property {Function} warning * @property {Function} error */ /** - * @typedef {Object} RateLimitExceeded - * @property {Boolean} isExceeded - * @property {String} [resetDate] - * @property {Number} [timeToWait] + * @typedef {object} RateLimitExceeded + * @property {boolean} isExceeded + * @property {string} [resetDate] + * @property {number} [timeToWait] */ /** @@ -697,7 +702,7 @@ function mapNodeToResult( node ) { */ /** - * @typedef {Object} SearchStaleIssuesOrPullRequestsResult + * @typedef {object} SearchStaleIssuesOrPullRequestsResult * @property {Array.} issuesOrPullRequestsToClose * @property {Array.} issuesOrPullRequestsToUnstale */ diff --git a/packages/ckeditor5-dev-stale-bot/lib/utils/findstaledate.js b/packages/ckeditor5-dev-stale-bot/lib/utils/findstaledate.js index 51d738c8e..4af89dbf1 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/utils/findstaledate.js +++ b/packages/ckeditor5-dev-stale-bot/lib/utils/findstaledate.js @@ -3,18 +3,16 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { isAfter, parseISO } = require( 'date-fns' ); +import { isAfter, parseISO } from 'date-fns'; /** * Finds the most recent event date of the stale label assignment to issue or pull request. * * @param {IssueOrPullRequest} issueOrPullRequest Issue or pull request to check. * @param {Options} options Configuration options. - * @returns {String} + * @returns {string} */ -module.exports = function findStaleDate( issueOrPullRequest, options ) { +export default function findStaleDate( issueOrPullRequest, options ) { const { staleLabels } = options; return issueOrPullRequest.timelineItems @@ -27,4 +25,4 @@ module.exports = function findStaleDate( issueOrPullRequest, options ) { } ) .find( entry => staleLabels.includes( entry.label ) ) .eventDate; -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequestactive.js b/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequestactive.js index 3193a3c82..3e591eed4 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequestactive.js +++ b/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequestactive.js @@ -3,9 +3,7 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { isAfter, parseISO } = require( 'date-fns' ); +import { isAfter, parseISO } from 'date-fns'; /** * Verifies dates from an issue or pull request to check if some of them occurred after the provided moment, meaning that the issue or pull @@ -21,11 +19,11 @@ const { isAfter, parseISO } = require( 'date-fns' ); * Some activity entries may be ignored and not used in the calculation, if so specified in the configuration (e.g. the author of an event). * * @param {IssueOrPullRequest} issueOrPullRequest Issue or pull request to check. - * @param {String} staleDate Date specifying the moment of checking the activity in the issue or pull request. + * @param {string} staleDate Date specifying the moment of checking the activity in the issue or pull request. * @param {Options} options Configuration options. - * @returns {Boolean} + * @returns {boolean} */ -module.exports = function isIssueOrPullRequestActive( issueOrPullRequest, staleDate, options ) { +export default function isIssueOrPullRequestActive( issueOrPullRequest, staleDate, options ) { const { ignoredActivityLogins, ignoredActivityLabels } = options; const dates = [ @@ -53,4 +51,4 @@ module.exports = function isIssueOrPullRequestActive( issueOrPullRequest, staleD return dates .filter( Boolean ) .some( date => isAfter( parseISO( date ), parseISO( staleDate ) ) ); -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttoclose.js b/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttoclose.js index 9a024721f..b9f709d9f 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttoclose.js +++ b/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttoclose.js @@ -3,20 +3,18 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { isAfter, parseISO } = require( 'date-fns' ); -const isIssueOrPullRequestActive = require( './isissueorpullrequestactive' ); -const findStaleDate = require( './findstaledate' ); +import { isAfter, parseISO } from 'date-fns'; +import isIssueOrPullRequestActive from './isissueorpullrequestactive.js'; +import findStaleDate from './findstaledate.js'; /** * Checks whether the time to close a stale issue or pull request has passed and whether it is still inactive. * * @param {IssueOrPullRequest} issueOrPullRequest Issue or pull request to check. * @param {Options} options Configuration options. - * @returns {Boolean} + * @returns {boolean} */ -module.exports = function isIssueOrPullRequestToClose( issueOrPullRequest, options ) { +export default function isIssueOrPullRequestToClose( issueOrPullRequest, options ) { const staleDate = findStaleDate( issueOrPullRequest, options ); const hasTimeToClosePassed = isAfter( parseISO( options.closeDate ), parseISO( staleDate ) ); @@ -25,4 +23,4 @@ module.exports = function isIssueOrPullRequestToClose( issueOrPullRequest, optio } return !isIssueOrPullRequestActive( issueOrPullRequest, staleDate, options ); -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttostale.js b/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttostale.js index 42ad9ba0a..4b9d254af 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttostale.js +++ b/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttostale.js @@ -3,19 +3,17 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const isIssueOrPullRequestActive = require( './isissueorpullrequestactive' ); +import isIssueOrPullRequestActive from './isissueorpullrequestactive.js'; /** * Checks whether issue or pull request should be staled, because it was not active since the defined moment of time. * * @param {IssueOrPullRequest} issueOrPullRequest Issue or pull request to check. * @param {Options} options Configuration options. - * @returns {Boolean} + * @returns {boolean} */ -module.exports = function isIssueOrPullRequestToStale( issueOrPullRequest, options ) { +export default function isIssueOrPullRequestToStale( issueOrPullRequest, options ) { const { staleDate } = options; return !isIssueOrPullRequestActive( issueOrPullRequest, staleDate, options ); -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttounstale.js b/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttounstale.js index b287a2681..a9252c6ff 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttounstale.js +++ b/packages/ckeditor5-dev-stale-bot/lib/utils/isissueorpullrequesttounstale.js @@ -3,20 +3,18 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const isIssueOrPullRequestActive = require( './isissueorpullrequestactive' ); -const findStaleDate = require( './findstaledate' ); +import isIssueOrPullRequestActive from './isissueorpullrequestactive.js'; +import findStaleDate from './findstaledate.js'; /** * Checks whether issue or pull request should be unstaled, because it was active after it was staled. * * @param {IssueOrPullRequest} issueOrPullRequest Issue or pull request to check. * @param {Options} options Configuration options. - * @returns {Boolean} + * @returns {boolean} */ -module.exports = function isIssueOrPullRequestToUnstale( issueOrPullRequest, options ) { +export default function isIssueOrPullRequestToUnstale( issueOrPullRequest, options ) { const staleDate = findStaleDate( issueOrPullRequest, options ); return isIssueOrPullRequestActive( issueOrPullRequest, staleDate, options ); -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuestale.js b/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuestale.js index 8a1531466..8381ea826 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuestale.js +++ b/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuestale.js @@ -3,15 +3,13 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /** * Checks whether pending issue is already stale. * * @param {PendingIssue} pendingIssue Pending issue to check. * @param {Options} options Configuration options. - * @returns {Boolean} + * @returns {boolean} */ -module.exports = function isPendingIssueStale( pendingIssue, options ) { +export default function isPendingIssueStale( pendingIssue, options ) { return options.staleLabels.every( staleLabel => pendingIssue.labels.includes( staleLabel ) ); -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuetostale.js b/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuetostale.js index b8d10a7bb..13fd1b00c 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuetostale.js +++ b/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuetostale.js @@ -3,19 +3,17 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { isBefore, parseISO } = require( 'date-fns' ); -const isPendingIssueStale = require( './ispendingissuestale' ); +import { isBefore, parseISO } from 'date-fns'; +import isPendingIssueStale from './ispendingissuestale.js'; /** * Checks whether pending issue should be staled, because it was not answered by a community member since the defined moment of time. * * @param {PendingIssue} pendingIssue Pending issue to check. * @param {Options} options Configuration options. - * @returns {Boolean} + * @returns {boolean} */ -module.exports = function isPendingIssueToStale( pendingIssue, options ) { +export default function isPendingIssueToStale( pendingIssue, options ) { const { lastComment } = pendingIssue; const { staleDatePendingIssue } = options; @@ -32,4 +30,4 @@ module.exports = function isPendingIssueToStale( pendingIssue, options ) { } return isBefore( parseISO( lastComment.createdAt ), parseISO( staleDatePendingIssue ) ); -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuetounlabel.js b/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuetounlabel.js index 404472608..f72856545 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuetounlabel.js +++ b/packages/ckeditor5-dev-stale-bot/lib/utils/ispendingissuetounlabel.js @@ -3,18 +3,16 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /** * Checks whether pending issue should be unlabeled, because it was answered by a community member. * * @param {PendingIssue} pendingIssue Pending issue to check. - * @returns {Boolean} + * @returns {boolean} */ -module.exports = function isPendingIssueToUnlabel( pendingIssue ) { +export default function isPendingIssueToUnlabel( pendingIssue ) { if ( !pendingIssue.lastComment ) { return false; } return pendingIssue.lastComment.isExternal; -}; +} diff --git a/packages/ckeditor5-dev-stale-bot/lib/utils/preparesearchquery.js b/packages/ckeditor5-dev-stale-bot/lib/utils/preparesearchquery.js index 05a9238fc..7e2c9fff7 100644 --- a/packages/ckeditor5-dev-stale-bot/lib/utils/preparesearchquery.js +++ b/packages/ckeditor5-dev-stale-bot/lib/utils/preparesearchquery.js @@ -3,20 +3,18 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /** * Creates a query to search for issues or pull requests. * - * @param {Object} options - * @param {String} options.repositorySlug - * @param {String} [options.searchDate] + * @param {object} options + * @param {string} options.repositorySlug + * @param {string} [options.searchDate] * @param {'Issue'|'PullRequest'} [options.type] - * @param {Array.} [options.labels=[]] - * @param {Array.} [options.ignoredLabels=[]] - * @returns {String} + * @param {Array.} [options.labels=[]] + * @param {Array.} [options.ignoredLabels=[]] + * @returns {string} */ -module.exports = function prepareSearchQuery( options ) { +export default function prepareSearchQuery( options ) { const { repositorySlug, searchDate, @@ -36,7 +34,7 @@ module.exports = function prepareSearchQuery( options ) { ...labels.map( label => `label:${ label }` ), ...ignoredLabels.map( label => `-label:${ label }` ) ].filter( Boolean ).join( ' ' ); -}; +} function mapGitHubResourceType( type ) { const resourceMap = { diff --git a/packages/ckeditor5-dev-stale-bot/package.json b/packages/ckeditor5-dev-stale-bot/package.json index ba4ca732e..50b1cf328 100644 --- a/packages/ckeditor5-dev-stale-bot/package.json +++ b/packages/ckeditor5-dev-stale-bot/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-stale-bot", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "A stale bot is used to mark issues and pull requests that have not recently been updated.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -16,6 +16,7 @@ "node": ">=18.0.0", "npm": ">=5.7.1" }, + "type": "module", "files": [ "bin", "lib" @@ -24,25 +25,22 @@ "ckeditor5-dev-stale-bot": "bin/stale-bot.js" }, "dependencies": { - "@ckeditor/ckeditor5-dev-utils": "^43.0.0", - "chalk": "^4.1.0", - "date-fns": "^2.30.0", - "fs-extra": "^11.2.0", + "@ckeditor/ckeditor5-dev-utils": "^44.0.0-alpha.5", + "chalk": "^5.0.0", + "date-fns": "^4.0.0", + "fs-extra": "^11.0.0", "graphql": "^16.8.1", - "graphql-request": "^6.1.0", + "graphql-request": "^7.0.0", "minimist": "^1.2.8", - "ora": "^5.2.0", + "ora": "^8.0.0", "upath": "^2.0.1" }, "devDependencies": { - "chai": "^4.2.0", - "mocha": "^7.1.2", - "sinon": "^9.2.4", - "proxyquire": "^2.1.3" + "vitest": "^2.0.5" }, "scripts": { - "test": "mocha './tests/**/*.js' --timeout 10000", - "coverage": "nyc --reporter=lcov --reporter=text-summary yarn run test" + "test": "vitest run --config vitest.config.js", + "coverage": "vitest run --config vitest.config.js --coverage" }, "depcheckIgnore": [ "graphql" diff --git a/packages/ckeditor5-dev-stale-bot/tests/githubrepository.js b/packages/ckeditor5-dev-stale-bot/tests/githubrepository.js index a9d50d724..9757e6ed4 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/githubrepository.js +++ b/packages/ckeditor5-dev-stale-bot/tests/githubrepository.js @@ -3,48 +3,84 @@ * For licensing, see LICENSE.md. */ -const upath = require( 'upath' ); -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); +import upath from 'upath'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import prepareSearchQuery from '../lib/utils/preparesearchquery.js'; +import isIssueOrPullRequestToStale from '../lib/utils/isissueorpullrequesttostale.js'; +import isIssueOrPullRequestToUnstale from '../lib/utils/isissueorpullrequesttounstale.js'; +import isIssueOrPullRequestToClose from '../lib/utils/isissueorpullrequesttoclose.js'; +import isPendingIssueToStale from '../lib/utils/ispendingissuetostale.js'; +import isPendingIssueToUnlabel from '../lib/utils/ispendingissuetounlabel.js'; +import GitHubRepository from '../lib/githubrepository.js'; + +vi.mock( '../lib/utils/preparesearchquery' ); +vi.mock( '../lib/utils/isissueorpullrequesttostale' ); +vi.mock( '../lib/utils/isissueorpullrequesttounstale' ); +vi.mock( '../lib/utils/isissueorpullrequesttoclose' ); +vi.mock( '../lib/utils/ispendingissuetostale' ); +vi.mock( '../lib/utils/ispendingissuetounlabel' ); + +const { + fsReadFileMock, + loggerInfoMock, + loggerErrorMock, + graphQLClientConstructorSpy, + graphQLClientRequestMock +} = vi.hoisted( () => { + return { + fsReadFileMock: vi.fn(), + loggerInfoMock: vi.fn(), + loggerErrorMock: vi.fn(), + graphQLClientConstructorSpy: vi.fn(), + graphQLClientRequestMock: vi.fn() + + }; +} ); + +vi.mock( 'fs-extra', () => { + return { + default: { + readFile: fsReadFileMock + } + }; +} ); + +vi.mock( '@ckeditor/ckeditor5-dev-utils', () => { + return { + logger: () => { + return { + info: loggerInfoMock, + error: loggerErrorMock + }; + } + }; +} ); + +vi.mock( 'graphql-request', () => { + return { + GraphQLClient: class { + constructor( ...args ) { + graphQLClientConstructorSpy( ...args ); -const GRAPHQL_PATH = upath.join( __dirname, '..', 'lib', 'graphql' ); + this.request = graphQLClientRequestMock; + } + } + }; +} ); describe( 'dev-stale-bot/lib', () => { describe( 'GitHubRepository', () => { - let githubRepository, pageInfoNoNextPage, pageInfoWithNextPage, stubs, sandbox; + let githubRepository, pageInfoNoNextPage, pageInfoWithNextPage; beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - fs: { - readFile: sinon.stub() - }, - logger: { - error: sinon.stub(), - info: sinon.stub() - }, - GraphQLClient: { - class: class { - constructor( ...args ) { - stubs.GraphQLClient.constructor( ...args ); - } + vi.mocked( prepareSearchQuery ).mockReturnValue( 'search query' ); + vi.mocked( isIssueOrPullRequestToStale ).mockReturnValue( true ); + vi.mocked( isIssueOrPullRequestToUnstale ).mockReturnValue( true ); + vi.mocked( isIssueOrPullRequestToClose ).mockReturnValue( true ); + vi.mocked( isPendingIssueToStale ).mockReturnValue( true ); + vi.mocked( isPendingIssueToUnlabel ).mockReturnValue( true ); - request( ...args ) { - return stubs.GraphQLClient.request( ...args ); - } - }, - constructor: sinon.stub(), - request: sinon.stub() - }, - prepareSearchQuery: sinon.stub().returns( 'search query' ), - isIssueOrPullRequestToStale: sinon.stub().returns( true ), - isIssueOrPullRequestToUnstale: sinon.stub().returns( true ), - isIssueOrPullRequestToClose: sinon.stub().returns( true ), - isPendingIssueToStale: sinon.stub().returns( true ), - isPendingIssueToUnlabel: sinon.stub().returns( true ) - }; + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( {} ); pageInfoWithNextPage = { hasNextPage: true, @@ -57,117 +93,113 @@ describe( 'dev-stale-bot/lib', () => { }; const queries = { - getviewerlogin: 'query GetViewerLogin', - searchissuesorpullrequests: 'query SearchIssuesOrPullRequests', - searchpendingissues: 'query SearchPendingIssues', - getissueorpullrequesttimelineitems: 'query GetIssueOrPullRequestTimelineItems', - addcomment: 'mutation AddComment', - getlabels: 'query GetLabels', - addlabels: 'mutation AddLabels', - removelabels: 'mutation RemoveLabels', - closeissue: 'mutation CloseIssue', - closepullrequest: 'mutation ClosePullRequest' + 'getviewerlogin.graphql': 'query GetViewerLogin', + 'searchissuesorpullrequests.graphql': 'query SearchIssuesOrPullRequests', + 'searchpendingissues.graphql': 'query SearchPendingIssues', + 'getissueorpullrequesttimelineitems.graphql': 'query GetIssueOrPullRequestTimelineItems', + 'addcomment.graphql': 'mutation AddComment', + 'getlabels.graphql': 'query GetLabels', + 'addlabels.graphql': 'mutation AddLabels', + 'removelabels.graphql': 'mutation RemoveLabels', + 'closeissue.graphql': 'mutation CloseIssue', + 'closepullrequest.graphql': 'mutation ClosePullRequest' }; - for ( const [ file, query ] of Object.entries( queries ) ) { - const absolutePath = upath.join( GRAPHQL_PATH, `${ file }.graphql` ); - - stubs.fs.readFile.withArgs( absolutePath, 'utf-8' ).resolves( query ); - } - - const GitHubRepository = proxyquire( '../lib/githubrepository', { - 'fs-extra': stubs.fs, - 'graphql-request': { - GraphQLClient: stubs.GraphQLClient.class - }, - '@ckeditor/ckeditor5-dev-utils': { - logger() { - return stubs.logger; - } - }, - './utils/preparesearchquery': stubs.prepareSearchQuery, - './utils/isissueorpullrequesttostale': stubs.isIssueOrPullRequestToStale, - './utils/isissueorpullrequesttounstale': stubs.isIssueOrPullRequestToUnstale, - './utils/isissueorpullrequesttoclose': stubs.isIssueOrPullRequestToClose, - './utils/ispendingissuetostale': stubs.isPendingIssueToStale, - './utils/ispendingissuetounlabel': stubs.isPendingIssueToUnlabel - } ); + vi.mocked( fsReadFileMock ).mockImplementation( path => queries[ upath.basename( path ) ] ); githubRepository = new GitHubRepository( 'authorization-token' ); } ); - afterEach( () => { - sandbox.restore(); - } ); - describe( '#constructor()', () => { it( 'should create a new instance of GraphQLClient', () => { - expect( stubs.GraphQLClient.constructor.callCount ).to.equal( 1 ); + expect( graphQLClientConstructorSpy ).toHaveBeenCalledTimes( 1 ); } ); it( 'should pass the API URL to the GraphQLClient instance', () => { - expect( stubs.GraphQLClient.constructor.getCall( 0 ).args[ 0 ] ).to.equal( 'https://api.github.com/graphql' ); + expect( graphQLClientConstructorSpy ).toHaveBeenCalledWith( + 'https://api.github.com/graphql', + expect.any( Object ) + ); } ); it( 'should pass the authorization token to the GraphQLClient instance', () => { - expect( stubs.GraphQLClient.constructor.getCall( 0 ).args[ 1 ] ).to.have.property( 'headers' ); - expect( stubs.GraphQLClient.constructor.getCall( 0 ).args[ 1 ].headers ).to.have.property( - 'Authorization', - 'Bearer authorization-token' + expect( graphQLClientConstructorSpy ).toHaveBeenCalledWith( + expect.any( String ), + expect.objectContaining( { + headers: expect.objectContaining( { + Authorization: 'Bearer authorization-token' + } ) + } ) ); } ); it( 'should pass a proper "Accept" header to the GraphQLClient instance', () => { - expect( stubs.GraphQLClient.constructor.getCall( 0 ).args[ 1 ] ).to.have.property( 'headers' ); - expect( stubs.GraphQLClient.constructor.getCall( 0 ).args[ 1 ].headers ).to.have.property( - 'Accept', - 'application/vnd.github.bane-preview+json' + expect( graphQLClientConstructorSpy ).toHaveBeenCalledWith( + expect.any( String ), + expect.objectContaining( { + headers: expect.objectContaining( { + Accept: 'application/vnd.github.bane-preview+json' + } ) + } ) ); } ); it( 'should switch to the new global GitHub ID namespace in the GraphQLClient instance', () => { - expect( stubs.GraphQLClient.constructor.getCall( 0 ).args[ 1 ] ).to.have.property( 'headers' ); - expect( stubs.GraphQLClient.constructor.getCall( 0 ).args[ 1 ].headers ).to.have.property( 'X-Github-Next-Global-ID', 1 ); + expect( graphQLClientConstructorSpy ).toHaveBeenCalledWith( + expect.any( String ), + expect.objectContaining( { + headers: expect.objectContaining( { + 'X-Github-Next-Global-ID': 1 + } ) + } ) + ); } ); it( 'should disable the cache in the GraphQLClient instance', () => { - expect( stubs.GraphQLClient.constructor.getCall( 0 ).args[ 1 ] ).to.have.property( 'cache', 'no-store' ); + expect( graphQLClientConstructorSpy ).toHaveBeenCalledWith( + expect.any( String ), + expect.objectContaining( { + cache: 'no-store' + } ) + ); } ); } ); describe( '#getViewerLogin()', () => { it( 'should be a function', () => { - expect( githubRepository.getViewerLogin ).to.be.a( 'function' ); + expect( githubRepository.getViewerLogin ).toBeInstanceOf( Function ); } ); - it( 'should return viewer login', () => { - stubs.GraphQLClient.request.resolves( { + it( 'should return viewer login', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { viewer: { login: 'CKEditorBot' } } ); - return githubRepository.getViewerLogin().then( result => { - expect( result ).to.equal( 'CKEditorBot' ); - } ); + const result = await githubRepository.getViewerLogin(); + + expect( result ).toEqual( 'CKEditorBot' ); } ); - it( 'should send one request for viewer login', () => { - stubs.GraphQLClient.request.resolves( { + it( 'should send one request for viewer login', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { viewer: { login: 'CKEditorBot' } } ); - return githubRepository.getViewerLogin().then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query GetViewerLogin' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( {} ); - } ); + await githubRepository.getViewerLogin(); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'query GetViewerLogin', + {} + ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.getViewerLogin().then( () => { @@ -182,18 +214,18 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.getViewerLogin().then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( - 'Unexpected error when executing "#getViewerLogin()".' + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#getViewerLogin()".', + error ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); } ); } ); @@ -201,17 +233,17 @@ describe( 'dev-stale-bot/lib', () => { describe( '#getIssueOrPullRequestTimelineItems()', () => { it( 'should be a function', () => { - expect( githubRepository.getIssueOrPullRequestTimelineItems ).to.be.a( 'function' ); + expect( githubRepository.getIssueOrPullRequestTimelineItems ).toBeInstanceOf( Function ); } ); - it( 'should return all timeline events if they are not paginated', () => { + it( 'should return all timeline events if they are not paginated', async () => { const timelineItems = [ { createdAt: '2022-12-01T09:00:00Z' }, { createdAt: '2022-12-02T09:00:00Z' }, { createdAt: '2022-12-03T09:00:00Z' } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { node: { timelineItems: { nodes: timelineItems, @@ -220,16 +252,16 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ).then( result => { - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 3 ); - expect( result[ 0 ] ).to.deep.equal( { eventDate: '2022-12-01T09:00:00Z' } ); - expect( result[ 1 ] ).to.deep.equal( { eventDate: '2022-12-02T09:00:00Z' } ); - expect( result[ 2 ] ).to.deep.equal( { eventDate: '2022-12-03T09:00:00Z' } ); - } ); + const result = await githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ); + + expect( result ).toEqual( [ + { eventDate: '2022-12-01T09:00:00Z' }, + { eventDate: '2022-12-02T09:00:00Z' }, + { eventDate: '2022-12-03T09:00:00Z' } + ] ); } ); - it( 'should return all timeline events if they are paginated', () => { + it( 'should return all timeline events if they are paginated', async () => { const timelineItems = [ { createdAt: '2022-12-01T09:00:00Z' }, { createdAt: '2022-12-02T09:00:00Z' }, @@ -247,23 +279,23 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ).then( result => { - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 3 ); - expect( result[ 0 ] ).to.deep.equal( { eventDate: '2022-12-01T09:00:00Z' } ); - expect( result[ 1 ] ).to.deep.equal( { eventDate: '2022-12-02T09:00:00Z' } ); - expect( result[ 2 ] ).to.deep.equal( { eventDate: '2022-12-03T09:00:00Z' } ); - } ); + const result = await githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ); + + expect( result ).toEqual( [ + { eventDate: '2022-12-01T09:00:00Z' }, + { eventDate: '2022-12-02T09:00:00Z' }, + { eventDate: '2022-12-03T09:00:00Z' } + ] ); } ); - it( 'should send one request for all timeline events if they are not paginated', () => { + it( 'should send one request for all timeline events if they are not paginated', async () => { const timelineItems = [ { createdAt: '2022-12-01T09:00:00Z' }, { createdAt: '2022-12-02T09:00:00Z' }, { createdAt: '2022-12-03T09:00:00Z' } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { node: { timelineItems: { nodes: timelineItems, @@ -272,14 +304,16 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query GetIssueOrPullRequestTimelineItems' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( { nodeId: 'IssueId', cursor: null } ); - } ); + await githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'query GetIssueOrPullRequestTimelineItems', + { nodeId: 'IssueId', cursor: null } + ); } ); - it( 'should send multiple requests for all timeline events if they are paginated', () => { + it( 'should send multiple requests for all timeline events if they are paginated', async () => { const timelineItems = [ { createdAt: '2022-12-01T09:00:00Z' }, { createdAt: '2022-12-02T09:00:00Z' }, @@ -297,21 +331,27 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ).then( () => { - expect( stubs.GraphQLClient.request.callCount ).to.equal( 3 ); + await githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query GetIssueOrPullRequestTimelineItems' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( { nodeId: 'IssueId', cursor: null } ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 3 ); - expect( stubs.GraphQLClient.request.getCall( 1 ).args[ 0 ] ).to.equal( 'query GetIssueOrPullRequestTimelineItems' ); - expect( stubs.GraphQLClient.request.getCall( 1 ).args[ 1 ] ).to.deep.equal( { nodeId: 'IssueId', cursor: 'cursor' } ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( 1, + 'query GetIssueOrPullRequestTimelineItems', + { nodeId: 'IssueId', cursor: null } + ); - expect( stubs.GraphQLClient.request.getCall( 2 ).args[ 0 ] ).to.equal( 'query GetIssueOrPullRequestTimelineItems' ); - expect( stubs.GraphQLClient.request.getCall( 2 ).args[ 1 ] ).to.deep.equal( { nodeId: 'IssueId', cursor: 'cursor' } ); - } ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( 2, + 'query GetIssueOrPullRequestTimelineItems', + { nodeId: 'IssueId', cursor: 'cursor' } + ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( 3, + 'query GetIssueOrPullRequestTimelineItems', + { nodeId: 'IssueId', cursor: 'cursor' } + ); } ); - it( 'should return event date, event author and label if any of these exist', () => { + it( 'should return event date, event author and label if any of these exist', async () => { const timelineItems = [ { createdAt: '2022-12-01T09:00:00Z' }, { updatedAt: '2022-12-02T09:00:00Z' }, @@ -320,7 +360,7 @@ describe( 'dev-stale-bot/lib', () => { { createdAt: '2022-12-05T09:00:00Z', label: { name: 'type:bug' } } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { node: { timelineItems: { nodes: timelineItems, @@ -329,19 +369,19 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ).then( result => { - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 5 ); - expect( result[ 0 ] ).to.deep.equal( { eventDate: '2022-12-01T09:00:00Z' } ); - expect( result[ 1 ] ).to.deep.equal( { eventDate: '2022-12-02T09:00:00Z' } ); - expect( result[ 2 ] ).to.deep.equal( { eventDate: '2022-12-03T09:00:00Z', author: 'RandomUser' } ); - expect( result[ 3 ] ).to.deep.equal( { eventDate: '2022-12-04T09:00:00Z', author: 'RandomUser' } ); - expect( result[ 4 ] ).to.deep.equal( { eventDate: '2022-12-05T09:00:00Z', label: 'type:bug' } ); - } ); + const result = await githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ); + + expect( result ).toEqual( [ + { eventDate: '2022-12-01T09:00:00Z' }, + { eventDate: '2022-12-02T09:00:00Z' }, + { eventDate: '2022-12-03T09:00:00Z', author: 'RandomUser' }, + { eventDate: '2022-12-04T09:00:00Z', author: 'RandomUser' }, + { eventDate: '2022-12-05T09:00:00Z', label: 'type:bug' } + ] ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ).then( () => { @@ -357,7 +397,7 @@ describe( 'dev-stale-bot/lib', () => { const timelineItems = [ { createdAt: '2022-12-01T09:00:00Z' }, { createdAt: '2022-12-02T09:00:00Z' }, - { createdAt: '2022-12-03T09:00:00Z' } + { error: new Error( '500 Internal Server Error' ) } ]; paginateRequest( timelineItems, ( { nodes, pageInfo } ) => { @@ -371,8 +411,6 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - stubs.GraphQLClient.request.onCall( 2 ).rejects( new Error( '500 Internal Server Error' ) ); - return githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ).then( () => { throw new Error( 'Expected to be rejected.' ); @@ -386,18 +424,17 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.getIssueOrPullRequestTimelineItems( 'IssueId' ).then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( - 'Unexpected error when executing "#getIssueOrPullRequestTimelineItems()".' + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#getIssueOrPullRequestTimelineItems()".', error ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); } ); } ); @@ -407,7 +444,7 @@ describe( 'dev-stale-bot/lib', () => { let onProgress, optionsBase, issueBase; beforeEach( () => { - onProgress = sinon.stub(); + onProgress = vi.fn(); optionsBase = { repositorySlug: 'ckeditor/ckeditor5', @@ -447,10 +484,10 @@ describe( 'dev-stale-bot/lib', () => { describe( '#searchIssuesOrPullRequestsToStale()', () => { it( 'should be a function', () => { - expect( githubRepository.searchIssuesOrPullRequestsToStale ).to.be.a( 'function' ); + expect( githubRepository.searchIssuesOrPullRequestsToStale ).toBeInstanceOf( Function ); } ); - it( 'should ask for issue search query', () => { + it( 'should ask for issue search query', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -462,7 +499,7 @@ describe( 'dev-stale-bot/lib', () => { ignoredIssueLabels: [ 'support:1', 'support:2', 'support:3' ] }; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -470,18 +507,18 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', options, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.calledOnce ).to.equal( true ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.deep.equal( { - type: 'Issue', - searchDate: '2022-12-01', - repositorySlug: 'ckeditor/ckeditor5', - ignoredLabels: [ 'status:stale', 'support:1', 'support:2', 'support:3' ] - } ); + await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', options, onProgress ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledWith( { + type: 'Issue', + searchDate: '2022-12-01', + repositorySlug: 'ckeditor/ckeditor5', + ignoredLabels: [ 'status:stale', 'support:1', 'support:2', 'support:3' ] } ); } ); - it( 'should ask for pull request search query', () => { + it( 'should ask for pull request search query', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -493,7 +530,7 @@ describe( 'dev-stale-bot/lib', () => { ignoredPullRequestLabels: [ 'support:1', 'support:2', 'support:3' ] }; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -501,25 +538,25 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'PullRequest', options, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.calledOnce ).to.equal( true ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.deep.equal( { - type: 'PullRequest', - searchDate: '2022-12-01', - repositorySlug: 'ckeditor/ckeditor5', - ignoredLabels: [ 'status:stale', 'support:1', 'support:2', 'support:3' ] - } ); + await githubRepository.searchIssuesOrPullRequestsToStale( 'PullRequest', options, onProgress ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledWith( { + type: 'PullRequest', + searchDate: '2022-12-01', + repositorySlug: 'ckeditor/ckeditor5', + ignoredLabels: [ 'status:stale', 'support:1', 'support:2', 'support:3' ] } ); } ); - it( 'should start the search from stale date if search date is not set', () => { + it( 'should start the search from stale date if search date is not set', async () => { const options = { ...optionsBase, searchDate: undefined, staleDate: '2023-01-01' }; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: 0, nodes: [], @@ -527,20 +564,22 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', options, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.calledOnce ).to.equal( true ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.have.property( 'searchDate', '2023-01-01' ); - } ); + await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', options, onProgress ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledWith( + expect.objectContaining( { 'searchDate': '2023-01-01' } ) + ); } ); - it( 'should return all issues to stale if they are not paginated', () => { + it( 'should return all issues to stale if they are not paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, { ...issueBase, number: 3 } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -548,22 +587,16 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( result => { - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 3 ); - expect( result[ 0 ] ).to.deep.equal( - { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } - ); - expect( result[ 1 ] ).to.deep.equal( - { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } - ); - expect( result[ 2 ] ).to.deep.equal( - { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } - ); - } ); + const result = await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); + + expect( result ).toEqual( [ + { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' }, + { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' }, + { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } + ] ); } ); - it( 'should return all issues to stale if they are paginated', () => { + it( 'should return all issues to stale if they are paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -580,29 +613,23 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( result => { - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 3 ); - expect( result[ 0 ] ).to.deep.equal( - { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } - ); - expect( result[ 1 ] ).to.deep.equal( - { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } - ); - expect( result[ 2 ] ).to.deep.equal( - { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } - ); - } ); + const result = await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); + + expect( result ).toEqual( [ + { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' }, + { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' }, + { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } + ] ); } ); - it( 'should send one request for all issues to stale if they are not paginated', () => { + it( 'should send one request for all issues to stale if they are not paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, { ...issueBase, number: 3 } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -610,16 +637,15 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query SearchIssuesOrPullRequests' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: null } - ); - } ); + await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'query SearchIssuesOrPullRequests', { query: 'search query', cursor: null } + ); } ); - it( 'should send multiple requests for all issues to stale if they are paginated', () => { + it( 'should send multiple requests for all issues to stale if they are paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -636,27 +662,20 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( () => { - expect( stubs.GraphQLClient.request.callCount ).to.equal( 3 ); - - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query SearchIssuesOrPullRequests' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: null } - ); - - expect( stubs.GraphQLClient.request.getCall( 1 ).args[ 0 ] ).to.equal( 'query SearchIssuesOrPullRequests' ); - expect( stubs.GraphQLClient.request.getCall( 1 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: 'cursor' } - ); - - expect( stubs.GraphQLClient.request.getCall( 2 ).args[ 0 ] ).to.equal( 'query SearchIssuesOrPullRequests' ); - expect( stubs.GraphQLClient.request.getCall( 2 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: 'cursor' } - ); - } ); + await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( + 1, 'query SearchIssuesOrPullRequests', { query: 'search query', cursor: null } + ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( + 2, 'query SearchIssuesOrPullRequests', { query: 'search query', cursor: 'cursor' } + ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( + 3, 'query SearchIssuesOrPullRequests', { query: 'search query', cursor: 'cursor' } + ); } ); - it( 'should fetch all timeline events for any issue if they are paginated', () => { + it( 'should fetch all timeline events for any issue if they are paginated', async () => { const issues = [ { ...issueBase, number: 1, timelineItems: { nodes: [], @@ -664,9 +683,9 @@ describe( 'dev-stale-bot/lib', () => { } } ]; - sinon.stub( githubRepository, 'getIssueOrPullRequestTimelineItems' ).resolves( [] ); + githubRepository.getIssueOrPullRequestTimelineItems = vi.fn().mockResolvedValue( [] ); - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -674,18 +693,15 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( () => { - expect( githubRepository.getIssueOrPullRequestTimelineItems.callCount ).to.equal( 1 ); + await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); - expect( githubRepository.getIssueOrPullRequestTimelineItems.getCall( 0 ).args[ 0 ] ).to.equal( 'IssueId' ); - expect( githubRepository.getIssueOrPullRequestTimelineItems.getCall( 0 ).args[ 1 ] ).to.deep.equal( { - hasNextPage: true, - cursor: 'cursor' - } ); - } ); + expect( vi.mocked( githubRepository.getIssueOrPullRequestTimelineItems ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( githubRepository.getIssueOrPullRequestTimelineItems ) ).toHaveBeenCalledWith( + 'IssueId', { hasNextPage: true, cursor: 'cursor' } + ); } ); - it( 'should ask for a new search query with new offset if GitHub prevents going to the next page', () => { + it( 'should ask for a new search query with new offset if GitHub prevents going to the next page', async () => { const issues = [ { ...issueBase, number: 1, createdAt: '2022-11-01T09:00:00Z' }, { ...issueBase, number: 2, createdAt: '2022-10-01T09:00:00Z' }, @@ -702,15 +718,21 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.callCount ).to.equal( 3 ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.have.property( 'searchDate', '2022-12-01' ); - expect( stubs.prepareSearchQuery.getCall( 1 ).args[ 0 ] ).to.have.property( 'searchDate', '2022-11-01T09:00:00Z' ); - expect( stubs.prepareSearchQuery.getCall( 2 ).args[ 0 ] ).to.have.property( 'searchDate', '2022-10-01T09:00:00Z' ); - } ); + await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenNthCalledWith( + 1, expect.objectContaining( { searchDate: '2022-12-01' } ) + ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenNthCalledWith( + 2, expect.objectContaining( { searchDate: '2022-11-01T09:00:00Z' } ) + ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenNthCalledWith( + 3, expect.objectContaining( { searchDate: '2022-10-01T09:00:00Z' } ) + ); } ); - it( 'should return all issues to stale if GitHub prevents going to the next page', () => { + it( 'should return all issues to stale if GitHub prevents going to the next page', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -727,29 +749,25 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( result => { - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 3 ); - expect( result[ 0 ] ).to.deep.equal( - { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } - ); - expect( result[ 1 ] ).to.deep.equal( - { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } - ); - expect( result[ 2 ] ).to.deep.equal( - { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } - ); - } ); + const result = await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); + + expect( result ).toBeInstanceOf( Array ); + expect( result.length ).toEqual( 3 ); + expect( result ).toEqual( [ + { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' }, + { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' }, + { id: 'IssueId', title: 'IssueTitle', type: 'Issue', url: 'https://github.com/' } + ] ); } ); - it( 'should check each issue if it is stale', () => { + it( 'should check each issue if it is stale', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, { ...issueBase, number: 3 } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -757,32 +775,33 @@ describe( 'dev-stale-bot/lib', () => { } } ); - stubs.isIssueOrPullRequestToStale.onCall( 1 ).returns( false ); - - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( result => { - const expectedIssue = { - ...issueBase, - lastReactedAt: null, - timelineItems: [] - }; - - expect( stubs.isIssueOrPullRequestToStale.callCount ).to.equal( 3 ); + vi.mocked( isIssueOrPullRequestToStale ).mockReturnValueOnce( true ); + vi.mocked( isIssueOrPullRequestToStale ).mockReturnValueOnce( false ); - expect( stubs.isIssueOrPullRequestToStale.getCall( 0 ).args[ 0 ] ).to.deep.equal( { ...expectedIssue, number: 1 } ); - expect( stubs.isIssueOrPullRequestToStale.getCall( 0 ).args[ 1 ] ).to.deep.equal( optionsBase ); + const result = await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); - expect( stubs.isIssueOrPullRequestToStale.getCall( 1 ).args[ 0 ] ).to.deep.equal( { ...expectedIssue, number: 2 } ); - expect( stubs.isIssueOrPullRequestToStale.getCall( 1 ).args[ 1 ] ).to.deep.equal( optionsBase ); + const expectedIssue = { + ...issueBase, + lastReactedAt: null, + timelineItems: [] + }; - expect( stubs.isIssueOrPullRequestToStale.getCall( 2 ).args[ 0 ] ).to.deep.equal( { ...expectedIssue, number: 3 } ); - expect( stubs.isIssueOrPullRequestToStale.getCall( 2 ).args[ 1 ] ).to.deep.equal( optionsBase ); + expect( vi.mocked( isIssueOrPullRequestToStale ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( isIssueOrPullRequestToStale ) ).toHaveBeenNthCalledWith( + 1, { ...expectedIssue, number: 1 }, optionsBase + ); + expect( vi.mocked( isIssueOrPullRequestToStale ) ).toHaveBeenNthCalledWith( + 2, { ...expectedIssue, number: 2 }, optionsBase + ); + expect( vi.mocked( isIssueOrPullRequestToStale ) ).toHaveBeenNthCalledWith( + 3, { ...expectedIssue, number: 3 }, optionsBase + ); - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 2 ); - } ); + expect( result ).toBeInstanceOf( Array ); + expect( result.length ).toEqual( 2 ); } ); - it( 'should call on progress callback', () => { + it( 'should call on progress callback', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -799,16 +818,15 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( () => { - expect( onProgress.callCount ).to.equal( 3 ); + await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); - expect( onProgress.getCall( 0 ).args[ 0 ] ).to.deep.equal( { done: 1, total: 3 } ); - expect( onProgress.getCall( 1 ).args[ 0 ] ).to.deep.equal( { done: 2, total: 3 } ); - expect( onProgress.getCall( 2 ).args[ 0 ] ).to.deep.equal( { done: 3, total: 3 } ); - } ); + expect( vi.mocked( onProgress ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 1, { done: 1, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 2, { done: 2, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 3, { done: 3, total: 3 } ); } ); - it( 'should count total hits only once using the value from first response', () => { + it( 'should count total hits only once using the value from first response', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -825,17 +843,16 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( () => { - expect( onProgress.callCount ).to.equal( 3 ); + await githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ); - expect( onProgress.getCall( 0 ).args[ 0 ] ).to.deep.equal( { done: 1, total: 3 } ); - expect( onProgress.getCall( 1 ).args[ 0 ] ).to.deep.equal( { done: 2, total: 3 } ); - expect( onProgress.getCall( 2 ).args[ 0 ] ).to.deep.equal( { done: 3, total: 3 } ); - } ); + expect( vi.mocked( onProgress ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 1, { done: 1, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 2, { done: 2, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 3, { done: 3, total: 3 } ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( () => { @@ -851,7 +868,7 @@ describe( 'dev-stale-bot/lib', () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, - { ...issueBase, number: 3 } + { error: new Error( '500 Internal Server Error' ) } ]; paginateRequest( issues, ( { nodes, pageInfo } ) => { @@ -864,8 +881,6 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - stubs.GraphQLClient.request.onCall( 2 ).rejects( new Error( '500 Internal Server Error' ) ); - return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( () => { throw new Error( 'Expected to be rejected.' ); @@ -879,18 +894,17 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.searchIssuesOrPullRequestsToStale( 'Issue', optionsBase, onProgress ).then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( - 'Unexpected error when executing "#searchIssuesOrPullRequestsToStale()".' + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#searchIssuesOrPullRequestsToStale()".', error ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); } ); } ); @@ -898,17 +912,17 @@ describe( 'dev-stale-bot/lib', () => { describe( '#searchStaleIssuesOrPullRequests()', () => { it( 'should be a function', () => { - expect( githubRepository.searchStaleIssuesOrPullRequests ).to.be.a( 'function' ); + expect( githubRepository.searchStaleIssuesOrPullRequests ).toBeInstanceOf( Function ); } ); - it( 'should ask for search query', () => { + it( 'should ask for search query', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, { ...issueBase, number: 3 } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -916,24 +930,24 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.calledOnce ).to.equal( true ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.deep.equal( { - searchDate: undefined, - repositorySlug: 'ckeditor/ckeditor5', - labels: [ 'status:stale' ] - } ); + await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledWith( { + searchDate: undefined, + repositorySlug: 'ckeditor/ckeditor5', + labels: [ 'status:stale' ] } ); } ); - it( 'should not set the initial start date', () => { + it( 'should not set the initial start date', async () => { const options = { ...optionsBase, searchDate: undefined, staleDate: '2023-01-01' }; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: 0, nodes: [], @@ -941,20 +955,22 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchStaleIssuesOrPullRequests( options, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.calledOnce ).to.equal( true ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.have.property( 'searchDate', undefined ); - } ); + await githubRepository.searchStaleIssuesOrPullRequests( options, onProgress ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledWith( + expect.objectContaining( { 'searchDate': undefined } ) + ); } ); - it( 'should return all stale issues if they are not paginated', () => { + it( 'should return all stale issues if they are not paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, { ...issueBase, number: 3 } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -962,37 +978,23 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( result => { - expect( result ).to.have.property( 'issuesOrPullRequestsToClose' ); - expect( result ).to.have.property( 'issuesOrPullRequestsToUnstale' ); + const result = await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); - expect( result.issuesOrPullRequestsToClose ).to.be.an( 'array' ); - expect( result.issuesOrPullRequestsToClose ).to.have.length( 3 ); - expect( result.issuesOrPullRequestsToClose[ 0 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToClose[ 1 ] ).to.deep.equal( + expect( result ).toEqual( { + issuesOrPullRequestsToClose: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToClose[ 2 ] ).to.deep.equal( + ], + issuesOrPullRequestsToUnstale: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - - expect( result.issuesOrPullRequestsToUnstale ).to.be.an( 'array' ); - expect( result.issuesOrPullRequestsToUnstale ).to.have.length( 3 ); - expect( result.issuesOrPullRequestsToUnstale[ 0 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToUnstale[ 1 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToUnstale[ 2 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + ] } ); } ); - it( 'should return all stale issues if they are paginated', () => { + it( 'should return all stale issues if they are paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1009,44 +1011,30 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( result => { - expect( result ).to.have.property( 'issuesOrPullRequestsToClose' ); - expect( result ).to.have.property( 'issuesOrPullRequestsToUnstale' ); - - expect( result.issuesOrPullRequestsToClose ).to.be.an( 'array' ); - expect( result.issuesOrPullRequestsToClose ).to.have.length( 3 ); - expect( result.issuesOrPullRequestsToClose[ 0 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToClose[ 1 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToClose[ 2 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + const result = await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); - expect( result.issuesOrPullRequestsToUnstale ).to.be.an( 'array' ); - expect( result.issuesOrPullRequestsToUnstale ).to.have.length( 3 ); - expect( result.issuesOrPullRequestsToUnstale[ 0 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToUnstale[ 1 ] ).to.deep.equal( + expect( result ).toEqual( { + issuesOrPullRequestsToClose: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToUnstale[ 2 ] ).to.deep.equal( + ], + issuesOrPullRequestsToUnstale: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + ] } ); } ); - it( 'should send one request for all stale issues if they are not paginated', () => { + it( 'should send one request for all stale issues if they are not paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, { ...issueBase, number: 3 } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -1054,16 +1042,15 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query SearchIssuesOrPullRequests' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: null } - ); - } ); + await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'query SearchIssuesOrPullRequests', { query: 'search query', cursor: null } + ); } ); - it( 'should send multiple requests for all stale issues if they are paginated', () => { + it( 'should send multiple requests for all stale issues if they are paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1080,27 +1067,22 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { - expect( stubs.GraphQLClient.request.callCount ).to.equal( 3 ); + await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query SearchIssuesOrPullRequests' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: null } - ); - - expect( stubs.GraphQLClient.request.getCall( 1 ).args[ 0 ] ).to.equal( 'query SearchIssuesOrPullRequests' ); - expect( stubs.GraphQLClient.request.getCall( 1 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: 'cursor' } - ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 3 ); - expect( stubs.GraphQLClient.request.getCall( 2 ).args[ 0 ] ).to.equal( 'query SearchIssuesOrPullRequests' ); - expect( stubs.GraphQLClient.request.getCall( 2 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: 'cursor' } - ); - } ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( + 1, 'query SearchIssuesOrPullRequests', { query: 'search query', cursor: null } + ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( + 2, 'query SearchIssuesOrPullRequests', { query: 'search query', cursor: 'cursor' } + ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( + 3, 'query SearchIssuesOrPullRequests', { query: 'search query', cursor: 'cursor' } + ); } ); - it( 'should fetch all timeline events for any issue if they are paginated', () => { + it( 'should fetch all timeline events for any issue if they are paginated', async () => { const issues = [ { ...issueBase, number: 1, timelineItems: { nodes: [], @@ -1108,9 +1090,9 @@ describe( 'dev-stale-bot/lib', () => { } } ]; - sinon.stub( githubRepository, 'getIssueOrPullRequestTimelineItems' ).resolves( [] ); + githubRepository.getIssueOrPullRequestTimelineItems = vi.fn().mockResolvedValue( [] ); - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -1118,18 +1100,15 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { - expect( githubRepository.getIssueOrPullRequestTimelineItems.callCount ).to.equal( 1 ); + await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); - expect( githubRepository.getIssueOrPullRequestTimelineItems.getCall( 0 ).args[ 0 ] ).to.equal( 'IssueId' ); - expect( githubRepository.getIssueOrPullRequestTimelineItems.getCall( 0 ).args[ 1 ] ).to.deep.equal( { - hasNextPage: true, - cursor: 'cursor' - } ); - } ); + expect( vi.mocked( githubRepository.getIssueOrPullRequestTimelineItems ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( githubRepository.getIssueOrPullRequestTimelineItems ) ).toHaveBeenCalledWith( + 'IssueId', { hasNextPage: true, cursor: 'cursor' } + ); } ); - it( 'should ask for a new search query with new offset if GitHub prevents going to the next page', () => { + it( 'should ask for a new search query with new offset if GitHub prevents going to the next page', async () => { const issues = [ { ...issueBase, number: 1, createdAt: '2022-11-01T09:00:00Z' }, { ...issueBase, number: 2, createdAt: '2022-10-01T09:00:00Z' }, @@ -1146,15 +1125,21 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.callCount ).to.equal( 3 ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.have.property( 'searchDate', undefined ); - expect( stubs.prepareSearchQuery.getCall( 1 ).args[ 0 ] ).to.have.property( 'searchDate', '2022-11-01T09:00:00Z' ); - expect( stubs.prepareSearchQuery.getCall( 2 ).args[ 0 ] ).to.have.property( 'searchDate', '2022-10-01T09:00:00Z' ); - } ); + await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 3 ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenNthCalledWith( + 1, expect.objectContaining( { searchDate: undefined } ) + ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenNthCalledWith( + 2, expect.objectContaining( { searchDate: '2022-11-01T09:00:00Z' } ) + ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenNthCalledWith( + 3, expect.objectContaining( { searchDate: '2022-10-01T09:00:00Z' } ) + ); } ); - it( 'should return all stale issues if GitHub prevents going to the next page', () => { + it( 'should return all stale issues if GitHub prevents going to the next page', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1171,44 +1156,30 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( result => { - expect( result ).to.have.property( 'issuesOrPullRequestsToClose' ); - expect( result ).to.have.property( 'issuesOrPullRequestsToUnstale' ); - - expect( result.issuesOrPullRequestsToClose ).to.be.an( 'array' ); - expect( result.issuesOrPullRequestsToClose ).to.have.length( 3 ); - expect( result.issuesOrPullRequestsToClose[ 0 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToClose[ 1 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToClose[ 2 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + const result = await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); - expect( result.issuesOrPullRequestsToUnstale ).to.be.an( 'array' ); - expect( result.issuesOrPullRequestsToUnstale ).to.have.length( 3 ); - expect( result.issuesOrPullRequestsToUnstale[ 0 ] ).to.deep.equal( + expect( result ).toEqual( { + issuesOrPullRequestsToClose: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToUnstale[ 1 ] ).to.deep.equal( + ], + issuesOrPullRequestsToUnstale: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.issuesOrPullRequestsToUnstale[ 2 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + ] } ); } ); - it( 'should check each issue if it should be unstaled or closed', () => { + it( 'should check each issue if it should be unstaled or closed', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, { ...issueBase, number: 3 } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -1216,52 +1187,46 @@ describe( 'dev-stale-bot/lib', () => { } } ); - stubs.isIssueOrPullRequestToUnstale.onCall( 1 ).returns( false ); - stubs.isIssueOrPullRequestToClose.onCall( 1 ).returns( false ); - - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( result => { - const expectedIssue = { - ...issueBase, - lastReactedAt: null, - timelineItems: [] - }; - - expect( stubs.isIssueOrPullRequestToUnstale.callCount ).to.equal( 3 ); - - expect( stubs.isIssueOrPullRequestToUnstale.getCall( 0 ).args[ 0 ] ).to.deep.equal( - { ...expectedIssue, number: 1 } - ); - expect( stubs.isIssueOrPullRequestToUnstale.getCall( 0 ).args[ 1 ] ).to.deep.equal( optionsBase ); - - expect( stubs.isIssueOrPullRequestToUnstale.getCall( 1 ).args[ 0 ] ).to.deep.equal( - { ...expectedIssue, number: 2 } - ); - expect( stubs.isIssueOrPullRequestToUnstale.getCall( 1 ).args[ 1 ] ).to.deep.equal( optionsBase ); - - expect( stubs.isIssueOrPullRequestToUnstale.getCall( 2 ).args[ 0 ] ).to.deep.equal( - { ...expectedIssue, number: 3 } - ); - expect( stubs.isIssueOrPullRequestToUnstale.getCall( 2 ).args[ 1 ] ).to.deep.equal( optionsBase ); + vi.mocked( isIssueOrPullRequestToUnstale ).mockReturnValueOnce( false ); + vi.mocked( isIssueOrPullRequestToClose ).mockReturnValueOnce( false ); - expect( stubs.isIssueOrPullRequestToClose.callCount ).to.equal( 3 ); + const result = await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); - expect( stubs.isIssueOrPullRequestToClose.getCall( 0 ).args[ 0 ] ).to.deep.equal( { ...expectedIssue, number: 1 } ); - expect( stubs.isIssueOrPullRequestToClose.getCall( 0 ).args[ 1 ] ).to.deep.equal( optionsBase ); + const expectedIssue = { + ...issueBase, + lastReactedAt: null, + timelineItems: [] + }; - expect( stubs.isIssueOrPullRequestToClose.getCall( 1 ).args[ 0 ] ).to.deep.equal( { ...expectedIssue, number: 2 } ); - expect( stubs.isIssueOrPullRequestToClose.getCall( 1 ).args[ 1 ] ).to.deep.equal( optionsBase ); + expect( vi.mocked( isIssueOrPullRequestToUnstale ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( isIssueOrPullRequestToUnstale ) ).toHaveBeenNthCalledWith( + 1, { ...expectedIssue, number: 1 }, optionsBase + ); + expect( vi.mocked( isIssueOrPullRequestToUnstale ) ).toHaveBeenNthCalledWith( + 2, { ...expectedIssue, number: 2 }, optionsBase + ); + expect( vi.mocked( isIssueOrPullRequestToUnstale ) ).toHaveBeenNthCalledWith( + 3, { ...expectedIssue, number: 3 }, optionsBase + ); - expect( stubs.isIssueOrPullRequestToClose.getCall( 2 ).args[ 0 ] ).to.deep.equal( { ...expectedIssue, number: 3 } ); - expect( stubs.isIssueOrPullRequestToClose.getCall( 2 ).args[ 1 ] ).to.deep.equal( optionsBase ); + expect( vi.mocked( isIssueOrPullRequestToClose ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( isIssueOrPullRequestToClose ) ).toHaveBeenNthCalledWith( + 1, { ...expectedIssue, number: 1 }, optionsBase + ); + expect( vi.mocked( isIssueOrPullRequestToClose ) ).toHaveBeenNthCalledWith( + 2, { ...expectedIssue, number: 2 }, optionsBase + ); + expect( vi.mocked( isIssueOrPullRequestToClose ) ).toHaveBeenNthCalledWith( + 3, { ...expectedIssue, number: 3 }, optionsBase + ); - expect( result.issuesOrPullRequestsToUnstale ).to.be.an( 'array' ); - expect( result.issuesOrPullRequestsToUnstale ).to.have.length( 2 ); - expect( result.issuesOrPullRequestsToClose ).to.be.an( 'array' ); - expect( result.issuesOrPullRequestsToClose ).to.have.length( 2 ); + expect( result ).toEqual( { + issuesOrPullRequestsToUnstale: [ expect.anything(), expect.anything() ], + issuesOrPullRequestsToClose: [ expect.anything(), expect.anything() ] } ); } ); - it( 'should call on progress callback', () => { + it( 'should call on progress callback', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1278,16 +1243,16 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { - expect( onProgress.callCount ).to.equal( 3 ); + await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); - expect( onProgress.getCall( 0 ).args[ 0 ] ).to.deep.equal( { done: 1, total: 3 } ); - expect( onProgress.getCall( 1 ).args[ 0 ] ).to.deep.equal( { done: 2, total: 3 } ); - expect( onProgress.getCall( 2 ).args[ 0 ] ).to.deep.equal( { done: 3, total: 3 } ); - } ); + expect( vi.mocked( onProgress ) ).toHaveBeenCalledTimes( 3 ); + + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 1, { done: 1, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 2, { done: 2, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 3, { done: 3, total: 3 } ); } ); - it( 'should count total hits only once using the value from first response', () => { + it( 'should count total hits only once using the value from first response', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1304,17 +1269,17 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { - expect( onProgress.callCount ).to.equal( 3 ); + await githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ); - expect( onProgress.getCall( 0 ).args[ 0 ] ).to.deep.equal( { done: 1, total: 3 } ); - expect( onProgress.getCall( 1 ).args[ 0 ] ).to.deep.equal( { done: 2, total: 3 } ); - expect( onProgress.getCall( 2 ).args[ 0 ] ).to.deep.equal( { done: 3, total: 3 } ); - } ); + expect( vi.mocked( onProgress ) ).toHaveBeenCalledTimes( 3 ); + + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 1, { done: 1, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 2, { done: 2, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 3, { done: 3, total: 3 } ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { @@ -1330,7 +1295,7 @@ describe( 'dev-stale-bot/lib', () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, - { ...issueBase, number: 3 } + { error: new Error( '500 Internal Server Error' ) } ]; paginateRequest( issues, ( { nodes, pageInfo } ) => { @@ -1343,8 +1308,6 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - stubs.GraphQLClient.request.onCall( 2 ).rejects( new Error( '500 Internal Server Error' ) ); - return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { throw new Error( 'Expected to be rejected.' ); @@ -1358,18 +1321,17 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.searchStaleIssuesOrPullRequests( optionsBase, onProgress ).then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( - 'Unexpected error when executing "#searchStaleIssuesOrPullRequests()".' + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#searchStaleIssuesOrPullRequests()".', error ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); } ); } ); @@ -1383,10 +1345,10 @@ describe( 'dev-stale-bot/lib', () => { } ); it( 'should be a function', () => { - expect( githubRepository.searchPendingIssues ).to.be.a( 'function' ); + expect( githubRepository.searchPendingIssues ).toBeInstanceOf( Function ); } ); - it( 'should ask for search query', () => { + it( 'should ask for search query', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1398,7 +1360,7 @@ describe( 'dev-stale-bot/lib', () => { ignoredIssueLabels: [ 'support:1', 'support:2', 'support:3' ] }; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -1406,26 +1368,26 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchPendingIssues( options, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.calledOnce ).to.equal( true ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.deep.equal( { - type: 'Issue', - searchDate: undefined, - repositorySlug: 'ckeditor/ckeditor5', - labels: [ 'pending:feedback' ], - ignoredLabels: [ 'support:1', 'support:2', 'support:3' ] - } ); + await githubRepository.searchPendingIssues( options, onProgress ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledWith( { + type: 'Issue', + searchDate: undefined, + repositorySlug: 'ckeditor/ckeditor5', + labels: [ 'pending:feedback' ], + ignoredLabels: [ 'support:1', 'support:2', 'support:3' ] } ); } ); - it( 'should not set the initial start date', () => { + it( 'should not set the initial start date', async () => { const options = { ...optionsBase, searchDate: undefined, staleDate: '2023-01-01' }; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: 0, nodes: [], @@ -1433,20 +1395,22 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchPendingIssues( options, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.calledOnce ).to.equal( true ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.have.property( 'searchDate', undefined ); - } ); + await githubRepository.searchPendingIssues( options, onProgress ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledWith( + expect.objectContaining( { searchDate: undefined } ) + ); } ); - it( 'should return all pending issues if they are not paginated', () => { + it( 'should return all pending issues if they are not paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, { ...issueBase, number: 3 } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -1454,37 +1418,23 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( result => { - expect( result ).to.have.property( 'pendingIssuesToStale' ); - expect( result ).to.have.property( 'pendingIssuesToUnlabel' ); - - expect( result.pendingIssuesToStale ).to.be.an( 'array' ); - expect( result.pendingIssuesToStale ).to.have.length( 3 ); - expect( result.pendingIssuesToStale[ 0 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToStale[ 1 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToStale[ 2 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + const result = await githubRepository.searchPendingIssues( optionsBase, onProgress ); - expect( result.pendingIssuesToUnlabel ).to.be.an( 'array' ); - expect( result.pendingIssuesToUnlabel ).to.have.length( 3 ); - expect( result.pendingIssuesToUnlabel[ 0 ] ).to.deep.equal( + expect( result ).toEqual( { + pendingIssuesToStale: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToUnlabel[ 1 ] ).to.deep.equal( + ], + pendingIssuesToUnlabel: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToUnlabel[ 2 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + ] } ); } ); - it( 'should return all pending issues if they are paginated', () => { + it( 'should return all pending issues if they are paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1501,44 +1451,30 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( result => { - expect( result ).to.have.property( 'pendingIssuesToStale' ); - expect( result ).to.have.property( 'pendingIssuesToUnlabel' ); - - expect( result.pendingIssuesToStale ).to.be.an( 'array' ); - expect( result.pendingIssuesToStale ).to.have.length( 3 ); - expect( result.pendingIssuesToStale[ 0 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToStale[ 1 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToStale[ 2 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + const result = await githubRepository.searchPendingIssues( optionsBase, onProgress ); - expect( result.pendingIssuesToUnlabel ).to.be.an( 'array' ); - expect( result.pendingIssuesToUnlabel ).to.have.length( 3 ); - expect( result.pendingIssuesToUnlabel[ 0 ] ).to.deep.equal( + expect( result ).toEqual( { + pendingIssuesToStale: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToUnlabel[ 1 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToUnlabel[ 2 ] ).to.deep.equal( + ], + pendingIssuesToUnlabel: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + ] } ); } ); - it( 'should send one request for all pending issues if they are not paginated', () => { + it( 'should send one request for all pending issues if they are not paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, { ...issueBase, number: 3 } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -1546,16 +1482,15 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query SearchPendingIssues' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: null } - ); - } ); + await githubRepository.searchPendingIssues( optionsBase, onProgress ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'query SearchPendingIssues', { query: 'search query', cursor: null } + ); } ); - it( 'should send multiple requests for all pending issues if they are paginated', () => { + it( 'should send multiple requests for all pending issues if they are paginated', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1572,27 +1507,21 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( () => { - expect( stubs.GraphQLClient.request.callCount ).to.equal( 3 ); + await githubRepository.searchPendingIssues( optionsBase, onProgress ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query SearchPendingIssues' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: null } - ); - - expect( stubs.GraphQLClient.request.getCall( 1 ).args[ 0 ] ).to.equal( 'query SearchPendingIssues' ); - expect( stubs.GraphQLClient.request.getCall( 1 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: 'cursor' } - ); - - expect( stubs.GraphQLClient.request.getCall( 2 ).args[ 0 ] ).to.equal( 'query SearchPendingIssues' ); - expect( stubs.GraphQLClient.request.getCall( 2 ).args[ 1 ] ).to.deep.equal( - { query: 'search query', cursor: 'cursor' } - ); - } ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( + 1, 'query SearchPendingIssues', { query: 'search query', cursor: null } + ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( + 2, 'query SearchPendingIssues', { query: 'search query', cursor: 'cursor' } + ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenNthCalledWith( + 3, 'query SearchPendingIssues', { query: 'search query', cursor: 'cursor' } + ); } ); - it( 'should ask for a new search query with new offset if GitHub prevents going to the next page', () => { + it( 'should ask for a new search query with new offset if GitHub prevents going to the next page', async () => { const issues = [ { ...issueBase, number: 1, createdAt: '2022-11-01T09:00:00Z' }, { ...issueBase, number: 2, createdAt: '2022-10-01T09:00:00Z' }, @@ -1609,15 +1538,21 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( () => { - expect( stubs.prepareSearchQuery.callCount ).to.equal( 3 ); - expect( stubs.prepareSearchQuery.getCall( 0 ).args[ 0 ] ).to.have.property( 'searchDate', undefined ); - expect( stubs.prepareSearchQuery.getCall( 1 ).args[ 0 ] ).to.have.property( 'searchDate', '2022-11-01T09:00:00Z' ); - expect( stubs.prepareSearchQuery.getCall( 2 ).args[ 0 ] ).to.have.property( 'searchDate', '2022-10-01T09:00:00Z' ); - } ); + await githubRepository.searchPendingIssues( optionsBase, onProgress ); + + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenNthCalledWith( + 1, expect.objectContaining( { searchDate: undefined } ) + ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenNthCalledWith( + 2, expect.objectContaining( { searchDate: '2022-11-01T09:00:00Z' } ) + ); + expect( vi.mocked( prepareSearchQuery ) ).toHaveBeenNthCalledWith( + 3, expect.objectContaining( { searchDate: '2022-10-01T09:00:00Z' } ) + ); } ); - it( 'should return all pending issues if GitHub prevents going to the next page', () => { + it( 'should return all pending issues if GitHub prevents going to the next page', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1634,37 +1569,23 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( result => { - expect( result ).to.have.property( 'pendingIssuesToStale' ); - expect( result ).to.have.property( 'pendingIssuesToUnlabel' ); + const result = await githubRepository.searchPendingIssues( optionsBase, onProgress ); - expect( result.pendingIssuesToStale ).to.be.an( 'array' ); - expect( result.pendingIssuesToStale ).to.have.length( 3 ); - expect( result.pendingIssuesToStale[ 0 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToStale[ 1 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToStale[ 2 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - - expect( result.pendingIssuesToUnlabel ).to.be.an( 'array' ); - expect( result.pendingIssuesToUnlabel ).to.have.length( 3 ); - expect( result.pendingIssuesToUnlabel[ 0 ] ).to.deep.equal( - { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToUnlabel[ 1 ] ).to.deep.equal( + expect( result ).toEqual( { + pendingIssuesToStale: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); - expect( result.pendingIssuesToUnlabel[ 2 ] ).to.deep.equal( + ], + pendingIssuesToUnlabel: [ + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, + { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' }, { id: 'IssueId', type: 'Issue', title: 'IssueTitle', url: 'https://github.com/' } - ); + ] } ); } ); - it( 'should check each issue if it should be staled or unlabeled', () => { + it( 'should check each issue if it should be staled or unlabeled', async () => { const commentMember = { createdAt: '2022-11-30T23:59:59Z', authorAssociation: 'MEMBER' @@ -1681,7 +1602,7 @@ describe( 'dev-stale-bot/lib', () => { { ...issueBase, number: 3, comments: { nodes: [ commentNonMember ] } } ]; - stubs.GraphQLClient.request.resolves( { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { search: { issueCount: issues.length, nodes: issues, @@ -1689,47 +1610,61 @@ describe( 'dev-stale-bot/lib', () => { } } ); - stubs.isPendingIssueToStale.onCall( 1 ).returns( false ); - stubs.isPendingIssueToUnlabel.onCall( 1 ).returns( false ); - - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( result => { - expect( stubs.isPendingIssueToStale.callCount ).to.equal( 3 ); - - expect( stubs.isPendingIssueToStale.getCall( 0 ).args[ 0 ] ).to.have.deep.property( 'lastComment', { - createdAt: '2022-11-30T23:59:59Z', isExternal: false - } ); - expect( stubs.isPendingIssueToStale.getCall( 0 ).args[ 1 ] ).to.deep.equal( optionsBase ); - - expect( stubs.isPendingIssueToStale.getCall( 1 ).args[ 0 ] ).to.have.deep.property( 'lastComment', { - createdAt: '2022-11-30T23:59:59Z', isExternal: false - } ); - expect( stubs.isPendingIssueToStale.getCall( 1 ).args[ 1 ] ).to.deep.equal( optionsBase ); - - expect( stubs.isPendingIssueToStale.getCall( 2 ).args[ 0 ] ).to.have.deep.property( 'lastComment', { - createdAt: '2022-11-30T23:59:59Z', isExternal: true - } ); - expect( stubs.isPendingIssueToStale.getCall( 2 ).args[ 1 ] ).to.deep.equal( optionsBase ); - - expect( stubs.isPendingIssueToUnlabel.callCount ).to.equal( 3 ); - - expect( stubs.isPendingIssueToUnlabel.getCall( 0 ).args[ 0 ] ).to.have.deep.property( 'lastComment', { - createdAt: '2022-11-30T23:59:59Z', isExternal: false - } ); - expect( stubs.isPendingIssueToUnlabel.getCall( 1 ).args[ 0 ] ).to.have.deep.property( 'lastComment', { - createdAt: '2022-11-30T23:59:59Z', isExternal: false - } ); - expect( stubs.isPendingIssueToUnlabel.getCall( 2 ).args[ 0 ] ).to.have.deep.property( 'lastComment', { - createdAt: '2022-11-30T23:59:59Z', isExternal: true - } ); - - expect( result.pendingIssuesToStale ).to.be.an( 'array' ); - expect( result.pendingIssuesToStale ).to.have.length( 2 ); - expect( result.pendingIssuesToUnlabel ).to.be.an( 'array' ); - expect( result.pendingIssuesToUnlabel ).to.have.length( 2 ); + vi.mocked( isPendingIssueToStale ).mockReturnValueOnce( false ); + vi.mocked( isPendingIssueToUnlabel ).mockReturnValueOnce( false ); + + const result = await githubRepository.searchPendingIssues( optionsBase, onProgress ); + + expect( vi.mocked( isPendingIssueToStale ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( isPendingIssueToStale ) ).toHaveBeenNthCalledWith( + 1, + expect.objectContaining( { + lastComment: { createdAt: '2022-11-30T23:59:59Z', isExternal: false } + } ), + optionsBase + ); + expect( vi.mocked( isPendingIssueToStale ) ).toHaveBeenNthCalledWith( + 2, + expect.objectContaining( { + lastComment: { createdAt: '2022-11-30T23:59:59Z', isExternal: false } + } ), + optionsBase + ); + expect( vi.mocked( isPendingIssueToStale ) ).toHaveBeenNthCalledWith( + 3, + expect.objectContaining( { + lastComment: { createdAt: '2022-11-30T23:59:59Z', isExternal: true } + } ), + optionsBase + ); + + expect( vi.mocked( isPendingIssueToUnlabel ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( isPendingIssueToUnlabel ) ).toHaveBeenNthCalledWith( + 1, + expect.objectContaining( { + lastComment: { createdAt: '2022-11-30T23:59:59Z', isExternal: false } + } ) + ); + expect( vi.mocked( isPendingIssueToUnlabel ) ).toHaveBeenNthCalledWith( + 2, + expect.objectContaining( { + lastComment: { createdAt: '2022-11-30T23:59:59Z', isExternal: false } + } ) + ); + expect( vi.mocked( isPendingIssueToUnlabel ) ).toHaveBeenNthCalledWith( + 3, + expect.objectContaining( { + lastComment: { createdAt: '2022-11-30T23:59:59Z', isExternal: true } + } ) + ); + + expect( result ).toEqual( { + pendingIssuesToStale: [ expect.anything(), expect.anything() ], + pendingIssuesToUnlabel: [ expect.anything(), expect.anything() ] } ); } ); - it( 'should call on progress callback', () => { + it( 'should call on progress callback', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1746,16 +1681,15 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( () => { - expect( onProgress.callCount ).to.equal( 3 ); + await githubRepository.searchPendingIssues( optionsBase, onProgress ); - expect( onProgress.getCall( 0 ).args[ 0 ] ).to.deep.equal( { done: 1, total: 3 } ); - expect( onProgress.getCall( 1 ).args[ 0 ] ).to.deep.equal( { done: 2, total: 3 } ); - expect( onProgress.getCall( 2 ).args[ 0 ] ).to.deep.equal( { done: 3, total: 3 } ); - } ); + expect( vi.mocked( onProgress ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 1, { done: 1, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 2, { done: 2, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 3, { done: 3, total: 3 } ); } ); - it( 'should count total hits only once using the value from first response', () => { + it( 'should count total hits only once using the value from first response', async () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, @@ -1772,17 +1706,16 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( () => { - expect( onProgress.callCount ).to.equal( 3 ); + await githubRepository.searchPendingIssues( optionsBase, onProgress ); - expect( onProgress.getCall( 0 ).args[ 0 ] ).to.deep.equal( { done: 1, total: 3 } ); - expect( onProgress.getCall( 1 ).args[ 0 ] ).to.deep.equal( { done: 2, total: 3 } ); - expect( onProgress.getCall( 2 ).args[ 0 ] ).to.deep.equal( { done: 3, total: 3 } ); - } ); + expect( vi.mocked( onProgress ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 1, { done: 1, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 2, { done: 2, total: 3 } ); + expect( vi.mocked( onProgress ) ).toHaveBeenNthCalledWith( 3, { done: 3, total: 3 } ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( () => { @@ -1798,7 +1731,7 @@ describe( 'dev-stale-bot/lib', () => { const issues = [ { ...issueBase, number: 1 }, { ...issueBase, number: 2 }, - { ...issueBase, number: 3 } + { error: new Error( '500 Internal Server Error' ) } ]; paginateRequest( issues, ( { nodes, pageInfo } ) => { @@ -1811,8 +1744,6 @@ describe( 'dev-stale-bot/lib', () => { }; } ); - stubs.GraphQLClient.request.onCall( 2 ).rejects( new Error( '500 Internal Server Error' ) ); - return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( () => { throw new Error( 'Expected to be rejected.' ); @@ -1826,18 +1757,17 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.searchPendingIssues( optionsBase, onProgress ).then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( - 'Unexpected error when executing "#searchPendingIssues()".' + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#searchPendingIssues()".', error ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); } ); } ); @@ -1846,23 +1776,22 @@ describe( 'dev-stale-bot/lib', () => { describe( '#addComment()', () => { it( 'should be a function', () => { - expect( githubRepository.addComment ).to.be.a( 'function' ); + expect( githubRepository.addComment ).toBeInstanceOf( Function ); } ); - it( 'should add a comment', () => { - stubs.GraphQLClient.request.resolves(); + it( 'should add a comment', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue(); - return githubRepository.addComment( 'IssueId', 'A comment.' ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'mutation AddComment' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( - { nodeId: 'IssueId', comment: 'A comment.' } - ); - } ); + await githubRepository.addComment( 'IssueId', 'A comment.' ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'mutation AddComment', { nodeId: 'IssueId', comment: 'A comment.' } + ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.addComment( 'IssueId', 'A comment.' ).then( () => { @@ -1877,16 +1806,17 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.addComment( 'IssueId', 'A comment.' ).then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( 'Unexpected error when executing "#addComment()".' ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#addComment()".', error + ); } ); } ); @@ -1904,18 +1834,17 @@ describe( 'dev-stale-bot/lib', () => { } ); it( 'should be a function', () => { - expect( githubRepository.getLabels ).to.be.a( 'function' ); + expect( githubRepository.getLabels ).toBeInstanceOf( Function ); } ); - it( 'should return an empty array if no labels are provided', () => { - return githubRepository.getLabels( 'ckeditor/ckeditor5', [] ).then( result => { - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 0 ); - } ); + it( 'should return an empty array if no labels are provided', async () => { + const result = await githubRepository.getLabels( 'ckeditor/ckeditor5', [] ); + + expect( result ).toEqual( [] ); } ); - it( 'should return labels', () => { - stubs.GraphQLClient.request.resolves( { + it( 'should return labels', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { repository: { labels: { nodes: labels @@ -1923,17 +1852,15 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.getLabels( 'ckeditor/ckeditor5', [ 'type:bug', 'type:task', 'type:feature' ] ).then( result => { - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 3 ); - expect( result[ 0 ] ).to.equal( 'LabelId1' ); - expect( result[ 1 ] ).to.equal( 'LabelId2' ); - expect( result[ 2 ] ).to.equal( 'LabelId3' ); - } ); + const result = await githubRepository.getLabels( 'ckeditor/ckeditor5', [ 'type:bug', 'type:task', 'type:feature' ] ); + + expect( result ).toEqual( [ + 'LabelId1', 'LabelId2', 'LabelId3' + ] ); } ); - it( 'should return only requested labels even if GitHub endpoint returned additional ones', () => { - stubs.GraphQLClient.request.resolves( { + it( 'should return only requested labels even if GitHub endpoint returned additional ones', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { repository: { labels: { nodes: [ @@ -1947,17 +1874,15 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.getLabels( 'ckeditor/ckeditor5', [ 'type:bug', 'type:task', 'type:feature' ] ).then( result => { - expect( result ).to.be.an( 'array' ); - expect( result ).to.have.length( 3 ); - expect( result[ 0 ] ).to.equal( 'LabelId1' ); - expect( result[ 1 ] ).to.equal( 'LabelId2' ); - expect( result[ 2 ] ).to.equal( 'LabelId3' ); - } ); + const result = await githubRepository.getLabels( 'ckeditor/ckeditor5', [ 'type:bug', 'type:task', 'type:feature' ] ); + + expect( result ).toEqual( [ + 'LabelId1', 'LabelId2', 'LabelId3' + ] ); } ); - it( 'should send one request for labels', () => { - stubs.GraphQLClient.request.resolves( { + it( 'should send one request for labels', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( { repository: { labels: { nodes: labels @@ -1965,19 +1890,16 @@ describe( 'dev-stale-bot/lib', () => { } } ); - return githubRepository.getLabels( 'ckeditor/ckeditor5', [ 'type:bug', 'type:task', 'type:feature' ] ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'query GetLabels' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( { - repositoryOwner: 'ckeditor', - repositoryName: 'ckeditor5', - labelNames: 'type:bug type:task type:feature' - } ); - } ); + await githubRepository.getLabels( 'ckeditor/ckeditor5', [ 'type:bug', 'type:task', 'type:feature' ] ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'query GetLabels', + { repositoryOwner: 'ckeditor', repositoryName: 'ckeditor5', labelNames: 'type:bug type:task type:feature' } ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.getLabels( 'ckeditor/ckeditor5', [ 'type:bug', 'type:task', 'type:feature' ] ).then( () => { @@ -1992,16 +1914,17 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.getLabels( 'ckeditor/ckeditor5', [ 'type:bug', 'type:task', 'type:feature' ] ).then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( 'Unexpected error when executing "#getLabels()".' ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#getLabels()".', error + ); } ); } ); @@ -2009,24 +1932,22 @@ describe( 'dev-stale-bot/lib', () => { describe( '#addLabels()', () => { it( 'should be a function', () => { - expect( githubRepository.addLabels ).to.be.a( 'function' ); + expect( githubRepository.addLabels ).toBeInstanceOf( Function ); } ); - it( 'should add a comment', () => { - stubs.GraphQLClient.request.resolves(); + it( 'should add a comment', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue(); - return githubRepository.addLabels( 'IssueId', [ 'LabelId' ] ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'mutation AddLabels' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( { - nodeId: 'IssueId', - labelIds: [ 'LabelId' ] - } ); - } ); + await githubRepository.addLabels( 'IssueId', [ 'LabelId' ] ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'mutation AddLabels', { nodeId: 'IssueId', labelIds: [ 'LabelId' ] } + ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.addLabels( 'IssueId', [ 'LabelId' ] ).then( () => { @@ -2041,16 +1962,17 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.addLabels( 'IssueId', [ 'LabelId' ] ).then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( 'Unexpected error when executing "#addLabels()".' ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#addLabels()".', error + ); } ); } ); @@ -2058,24 +1980,22 @@ describe( 'dev-stale-bot/lib', () => { describe( '#removeLabels()', () => { it( 'should be a function', () => { - expect( githubRepository.removeLabels ).to.be.a( 'function' ); + expect( githubRepository.removeLabels ).toBeInstanceOf( Function ); } ); - it( 'should add a comment', () => { - stubs.GraphQLClient.request.resolves(); + it( 'should add a comment', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue(); - return githubRepository.removeLabels( 'IssueId', [ 'LabelId' ] ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'mutation RemoveLabels' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( { - nodeId: 'IssueId', - labelIds: [ 'LabelId' ] - } ); - } ); + await githubRepository.removeLabels( 'IssueId', [ 'LabelId' ] ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'mutation RemoveLabels', { nodeId: 'IssueId', labelIds: [ 'LabelId' ] } + ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.removeLabels( 'IssueId', [ 'LabelId' ] ).then( () => { @@ -2090,18 +2010,17 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.removeLabels( 'IssueId', [ 'LabelId' ] ).then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( - 'Unexpected error when executing "#removeLabels()".' + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#removeLabels()".', error ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); } ); } ); @@ -2109,31 +2028,33 @@ describe( 'dev-stale-bot/lib', () => { describe( '#closeIssueOrPullRequest()', () => { it( 'should be a function', () => { - expect( githubRepository.closeIssueOrPullRequest ).to.be.a( 'function' ); + expect( githubRepository.closeIssueOrPullRequest ).toBeInstanceOf( Function ); } ); - it( 'should close issue', () => { - stubs.GraphQLClient.request.resolves(); + it( 'should close issue', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue(); - return githubRepository.closeIssueOrPullRequest( 'Issue', 'IssueId' ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'mutation CloseIssue' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( { nodeId: 'IssueId' } ); - } ); + await githubRepository.closeIssueOrPullRequest( 'Issue', 'IssueId' ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'mutation CloseIssue', { nodeId: 'IssueId' } + ); } ); - it( 'should close pull request', () => { - stubs.GraphQLClient.request.resolves(); + it( 'should close pull request', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue(); - return githubRepository.closeIssueOrPullRequest( 'PullRequest', 'PullRequestId' ).then( () => { - expect( stubs.GraphQLClient.request.calledOnce ).to.equal( true ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 0 ] ).to.equal( 'mutation ClosePullRequest' ); - expect( stubs.GraphQLClient.request.getCall( 0 ).args[ 1 ] ).to.deep.equal( { nodeId: 'PullRequestId' } ); - } ); + await githubRepository.closeIssueOrPullRequest( 'PullRequest', 'PullRequestId' ); + + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledWith( + 'mutation ClosePullRequest', { nodeId: 'PullRequestId' } + ); } ); it( 'should reject if request failed', () => { - stubs.GraphQLClient.request.rejects( new Error( '500 Internal Server Error' ) ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( new Error( '500 Internal Server Error' ) ); return githubRepository.closeIssueOrPullRequest( 'Issue', 'IssueId' ).then( () => { @@ -2148,18 +2069,17 @@ describe( 'dev-stale-bot/lib', () => { it( 'should log an error if request failed', () => { const error = new Error( '500 Internal Server Error' ); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.closeIssueOrPullRequest( 'Issue', 'IssueId' ).then( () => { throw new Error( 'Expected to be rejected.' ); }, () => { - expect( stubs.logger.error.callCount ).to.equal( 1 ); - expect( stubs.logger.error.getCall( 0 ).args[ 0 ] ).to.equal( - 'Unexpected error when executing "#closeIssueOrPullRequest()".' + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledWith( + 'Unexpected error when executing "#closeIssueOrPullRequest()".', error ); - expect( stubs.logger.error.getCall( 0 ).args[ 1 ] ).to.equal( error ); } ); } ); @@ -2172,39 +2092,37 @@ describe( 'dev-stale-bot/lib', () => { } }; - let clock; - beforeEach( () => { - clock = sinon.useFakeTimers(); + vi.useFakeTimers(); } ); afterEach( () => { - clock.restore(); + vi.useRealTimers(); } ); it( 'should be a function', () => { - expect( githubRepository.sendRequest ).to.be.a( 'function' ); + expect( githubRepository.sendRequest ).toBeInstanceOf( Function ); } ); - it( 'should resolve with the payload if no error occurred', () => { - stubs.GraphQLClient.request.resolves( payload ); + it( 'should resolve with the payload if no error occurred', async () => { + vi.mocked( graphQLClientRequestMock ).mockResolvedValue( payload ); - return githubRepository.sendRequest( 'query' ).then( result => { - expect( result ).to.equal( payload ); - } ); + const result = await githubRepository.sendRequest( 'query' ); + + expect( result ).toEqual( payload ); } ); it( 'should reject with the error - no custom properties', () => { const error = new Error(); - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.sendRequest( 'query' ).then( () => { throw new Error( 'Expected to be rejected.' ); }, err => { - expect( err ).to.equal( error ); + expect( err ).toEqual( error ); } ); } ); @@ -2213,14 +2131,14 @@ describe( 'dev-stale-bot/lib', () => { const error = new Error(); error.response = {}; - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.sendRequest( 'query' ).then( () => { throw new Error( 'Expected to be rejected.' ); }, err => { - expect( err ).to.equal( error ); + expect( err ).toEqual( error ); } ); } ); @@ -2231,14 +2149,14 @@ describe( 'dev-stale-bot/lib', () => { errors: [] }; - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.sendRequest( 'query' ).then( () => { throw new Error( 'Expected to be rejected.' ); }, err => { - expect( err ).to.equal( error ); + expect( err ).toEqual( error ); } ); } ); @@ -2251,14 +2169,14 @@ describe( 'dev-stale-bot/lib', () => { ] }; - stubs.GraphQLClient.request.rejects( error ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValue( error ); return githubRepository.sendRequest( 'query' ).then( () => { throw new Error( 'Expected to be rejected.' ); }, err => { - expect( err ).to.equal( error ); + expect( err ).toEqual( error ); } ); } ); @@ -2276,15 +2194,16 @@ describe( 'dev-stale-bot/lib', () => { headers: new Map( [ [ 'x-ratelimit-reset', resetTimestamp ] ] ) }; - stubs.GraphQLClient.request.onCall( 0 ).rejects( error ).onCall( 1 ).resolves( payload ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValueOnce( error ); + vi.mocked( graphQLClientRequestMock ).mockResolvedValueOnce( payload ); const sendPromise = githubRepository.sendRequest( 'query' ); - expect( stubs.GraphQLClient.request.callCount ).to.equal( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); - await clock.tickAsync( timeToWait * 1000 ); + await vi.advanceTimersByTimeAsync( timeToWait * 1000 ); - expect( stubs.GraphQLClient.request.callCount ).to.equal( 2 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 2 ); return sendPromise; } ); @@ -2297,17 +2216,18 @@ describe( 'dev-stale-bot/lib', () => { headers: new Map( [ [ 'retry-after', timeToWait ] ] ) }; - stubs.GraphQLClient.request.onCall( 0 ).rejects( error ).onCall( 1 ).resolves( payload ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValueOnce( error ); + vi.mocked( graphQLClientRequestMock ).mockResolvedValueOnce( payload ); const sendPromise = githubRepository.sendRequest( 'query' ); - expect( stubs.GraphQLClient.request.callCount ).to.equal( 1 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 1 ); - await clock.tickAsync( timeToWait * 1000 ); + await vi.advanceTimersByTimeAsync( timeToWait * 1000 ); - expect( stubs.GraphQLClient.request.callCount ).to.equal( 2 ); + expect( vi.mocked( graphQLClientRequestMock ) ).toHaveBeenCalledTimes( 2 ); - return sendPromise; + await sendPromise; } ); it( 'should log the progress and resolve with the payload after API rate is reset', async () => { @@ -2323,30 +2243,34 @@ describe( 'dev-stale-bot/lib', () => { headers: new Map( [ [ 'x-ratelimit-reset', resetTimestamp ] ] ) }; - stubs.GraphQLClient.request.onCall( 0 ).rejects( error ).onCall( 1 ).resolves( payload ); + vi.mocked( graphQLClientRequestMock ).mockRejectedValueOnce( error ); + vi.mocked( graphQLClientRequestMock ).mockResolvedValueOnce( payload ); const sendPromise = githubRepository.sendRequest( 'query' ); - await clock.tickAsync( 0 ); + await vi.advanceTimersByTimeAsync( 0 ); - expect( stubs.logger.info.callCount ).to.equal( 1 ); - expect( stubs.logger.info.getCall( 0 ).args[ 0 ] ).to.equal( + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenLastCalledWith( '⛔ The API limit is exceeded. Request is paused for 28 minutes.' ); - await clock.tickAsync( timeToWait * 1000 ); + await vi.advanceTimersByTimeAsync( timeToWait * 1000 ); - expect( stubs.logger.info.callCount ).to.equal( 2 ); - expect( stubs.logger.info.getCall( 1 ).args[ 0 ] ).to.equal( '📍 Re-sending postponed request.' ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenLastCalledWith( '📍 Re-sending postponed request.' ); - return sendPromise.then( result => { - expect( result ).to.equal( payload ); - } ); + const result = await sendPromise; + expect( result ).toEqual( payload ); } ); } ); function paginateRequest( dataToPaginate, paginator ) { for ( const entry of dataToPaginate ) { + if ( entry.error ) { + vi.mocked( graphQLClientRequestMock ).mockRejectedValueOnce( entry.error ); + } + const entryIndex = dataToPaginate.indexOf( entry ); const isLastEntry = entryIndex === dataToPaginate.length - 1; const pageInfo = isLastEntry ? pageInfoNoNextPage : pageInfoWithNextPage; @@ -2357,7 +2281,7 @@ describe( 'dev-stale-bot/lib', () => { entryIndex } ); - stubs.GraphQLClient.request.onCall( entryIndex ).resolves( result ); + vi.mocked( graphQLClientRequestMock ).mockResolvedValueOnce( result ); } } } ); diff --git a/packages/ckeditor5-dev-stale-bot/tests/utils/findstaledate.js b/packages/ckeditor5-dev-stale-bot/tests/utils/findstaledate.js index 53fbed7ea..ec04151b2 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/utils/findstaledate.js +++ b/packages/ckeditor5-dev-stale-bot/tests/utils/findstaledate.js @@ -3,8 +3,8 @@ * For licensing, see LICENSE.md. */ -const expect = require( 'chai' ).expect; -const findStaleDate = require( '../../lib/utils/findstaledate' ); +import { describe, it, expect, beforeEach } from 'vitest'; +import findStaleDate from '../../lib/utils/findstaledate.js'; describe( 'dev-stale-bot/lib/utils', () => { describe( 'findStaleDate', () => { @@ -17,7 +17,7 @@ describe( 'dev-stale-bot/lib/utils', () => { } ); it( 'should be a function', () => { - expect( findStaleDate ).to.be.a( 'function' ); + expect( findStaleDate ).toBeInstanceOf( Function ); } ); it( 'should return date when stale label was set', () => { @@ -27,7 +27,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ] }; - expect( findStaleDate( issue, optionsBase ) ).to.equal( '2022-12-01T00:00:00Z' ); + expect( findStaleDate( issue, optionsBase ) ).toEqual( '2022-12-01T00:00:00Z' ); } ); it( 'should return date when stale label was set if issue has multiple different events', () => { @@ -42,7 +42,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ] }; - expect( findStaleDate( issue, optionsBase ) ).to.equal( '2022-12-01T00:00:00Z' ); + expect( findStaleDate( issue, optionsBase ) ).toEqual( '2022-12-01T00:00:00Z' ); } ); it( 'should return most recent date when stale label was set if issue has multiple stale label events', () => { @@ -57,7 +57,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ] }; - expect( findStaleDate( issue, optionsBase ) ).to.equal( '2022-12-06T00:00:00Z' ); + expect( findStaleDate( issue, optionsBase ) ).toEqual( '2022-12-06T00:00:00Z' ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequestactive.js b/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequestactive.js index 7d17211d8..716163c4e 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequestactive.js +++ b/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequestactive.js @@ -3,8 +3,8 @@ * For licensing, see LICENSE.md. */ -const expect = require( 'chai' ).expect; -const isIssueOrPullRequestActive = require( '../../lib/utils/isissueorpullrequestactive' ); +import { describe, it, expect, beforeEach } from 'vitest'; +import isIssueOrPullRequestActive from '../../lib/utils/isissueorpullrequestactive.js'; describe( 'dev-stale-bot/lib/utils', () => { describe( 'isIssueOrPullRequestActive', () => { @@ -29,7 +29,7 @@ describe( 'dev-stale-bot/lib/utils', () => { } ); it( 'should be a function', () => { - expect( isIssueOrPullRequestActive ).to.be.a( 'function' ); + expect( isIssueOrPullRequestActive ).toBeInstanceOf( Function ); } ); it( 'should return true for issue created after stale date', () => { @@ -38,7 +38,7 @@ describe( 'dev-stale-bot/lib/utils', () => { createdAt: afterStaleDate }; - expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).to.be.true; + expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).toEqual( true ); } ); it( 'should return false for issue created before stale date', () => { @@ -47,7 +47,7 @@ describe( 'dev-stale-bot/lib/utils', () => { createdAt: beforeStaleDate }; - expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).to.be.false; + expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).toEqual( false ); } ); it( 'should return true for issue edited after stale date', () => { @@ -56,7 +56,7 @@ describe( 'dev-stale-bot/lib/utils', () => { lastEditedAt: afterStaleDate }; - expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).to.be.true; + expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).toEqual( true ); } ); it( 'should return false for issue edited before stale date', () => { @@ -65,7 +65,7 @@ describe( 'dev-stale-bot/lib/utils', () => { lastEditedAt: beforeStaleDate }; - expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).to.be.false; + expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).toEqual( false ); } ); it( 'should return true for issue with reaction after stale date', () => { @@ -74,7 +74,7 @@ describe( 'dev-stale-bot/lib/utils', () => { lastReactedAt: afterStaleDate }; - expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).to.be.true; + expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).toEqual( true ); } ); it( 'should return false for issue with reaction before stale date', () => { @@ -83,7 +83,7 @@ describe( 'dev-stale-bot/lib/utils', () => { lastReactedAt: beforeStaleDate }; - expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).to.be.false; + expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).toEqual( false ); } ); it( 'should return true for issue with activity after stale date', () => { @@ -95,7 +95,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ] }; - expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).to.be.true; + expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).toEqual( true ); } ); it( 'should return false for issue without activity after stale date', () => { @@ -107,7 +107,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ] }; - expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).to.be.false; + expect( isIssueOrPullRequestActive( issue, staleDate, optionsBase ) ).toEqual( false ); } ); it( 'should return true for issue with activity after stale date and its author is not ignored', () => { @@ -124,7 +124,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ignoredActivityLogins: [ 'CKEditorBot' ] }; - expect( isIssueOrPullRequestActive( issue, staleDate, options ) ).to.be.true; + expect( isIssueOrPullRequestActive( issue, staleDate, options ) ).toEqual( true ); } ); it( 'should return false for issue with activity after stale date but its author is ignored', () => { @@ -141,7 +141,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ignoredActivityLogins: [ 'CKEditorBot' ] }; - expect( isIssueOrPullRequestActive( issue, staleDate, options ) ).to.be.false; + expect( isIssueOrPullRequestActive( issue, staleDate, options ) ).toEqual( false ); } ); it( 'should return true for issue with activity after stale date and label is not ignored', () => { @@ -158,7 +158,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ignoredActivityLabels: [ 'status:stale' ] }; - expect( isIssueOrPullRequestActive( issue, staleDate, options ) ).to.be.true; + expect( isIssueOrPullRequestActive( issue, staleDate, options ) ).toEqual( true ); } ); it( 'should return false for issue with activity after stale date but label is ignored', () => { @@ -175,7 +175,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ignoredActivityLabels: [ 'status:stale' ] }; - expect( isIssueOrPullRequestActive( issue, staleDate, options ) ).to.be.false; + expect( isIssueOrPullRequestActive( issue, staleDate, options ) ).toEqual( false ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttoclose.js b/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttoclose.js index c578a0310..5ef1a0208 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttoclose.js +++ b/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttoclose.js @@ -3,13 +3,17 @@ * For licensing, see LICENSE.md. */ -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import isIssueOrPullRequestToClose from '../../lib/utils/isissueorpullrequesttoclose.js'; +import findStaleDate from '../../lib/utils/findstaledate.js'; +import isIssueOrPullRequestActive from '../../lib/utils/isissueorpullrequestactive.js'; + +vi.mock( '../../lib/utils/findstaledate' ); +vi.mock( '../../lib/utils/isissueorpullrequestactive' ); describe( 'dev-stale-bot/lib/utils', () => { describe( 'isIssueOrPullRequestToClose', () => { - let isIssueOrPullRequestToClose, staleDate, afterStaleDate, beforeStaleDate, issueBase, optionsBase, stubs; + let staleDate, afterStaleDate, beforeStaleDate, issueBase, optionsBase; beforeEach( () => { staleDate = '2022-12-01T00:00:00Z'; @@ -22,27 +26,18 @@ describe( 'dev-stale-bot/lib/utils', () => { closeDate: staleDate }; - stubs = { - findStaleDate: sinon.stub().returns( staleDate ), - isIssueOrPullRequestActive: sinon.stub() - }; - - isIssueOrPullRequestToClose = proxyquire( '../../lib/utils/isissueorpullrequesttoclose', { - './findstaledate': stubs.findStaleDate, - './isissueorpullrequestactive': stubs.isIssueOrPullRequestActive - } ); + vi.mocked( findStaleDate ).mockReturnValue( staleDate ); } ); it( 'should be a function', () => { - expect( isIssueOrPullRequestToClose ).to.be.a( 'function' ); + expect( isIssueOrPullRequestToClose ).toBeInstanceOf( Function ); } ); it( 'should get the stale date from issue activity', () => { isIssueOrPullRequestToClose( issueBase, optionsBase ); - expect( stubs.findStaleDate.calledOnce ).to.equal( true ); - expect( stubs.findStaleDate.getCall( 0 ).args[ 0 ] ).to.equal( issueBase ); - expect( stubs.findStaleDate.getCall( 0 ).args[ 1 ] ).to.equal( optionsBase ); + expect( vi.mocked( findStaleDate ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( findStaleDate ) ).toHaveBeenCalledWith( issueBase, optionsBase ); } ); it( 'should not check issue activity if time to close has not passed', () => { @@ -50,7 +45,7 @@ describe( 'dev-stale-bot/lib/utils', () => { isIssueOrPullRequestToClose( issueBase, optionsBase ); - expect( stubs.isIssueOrPullRequestActive.called ).to.equal( false ); + expect( vi.mocked( isIssueOrPullRequestActive ) ).not.toHaveBeenCalled(); } ); it( 'should check issue activity if time to close has passed', () => { @@ -58,38 +53,36 @@ describe( 'dev-stale-bot/lib/utils', () => { isIssueOrPullRequestToClose( issueBase, optionsBase ); - expect( stubs.isIssueOrPullRequestActive.calledOnce ).to.equal( true ); - expect( stubs.isIssueOrPullRequestActive.getCall( 0 ).args[ 0 ] ).to.equal( issueBase ); - expect( stubs.isIssueOrPullRequestActive.getCall( 0 ).args[ 1 ] ).to.equal( staleDate ); - expect( stubs.isIssueOrPullRequestActive.getCall( 0 ).args[ 2 ] ).to.equal( optionsBase ); + expect( vi.mocked( isIssueOrPullRequestActive ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( isIssueOrPullRequestActive ) ).toHaveBeenCalledWith( issueBase, staleDate, optionsBase ); } ); it( 'should return true if issue is not active after stale date and time to close has passed', () => { optionsBase.closeDate = afterStaleDate; - stubs.isIssueOrPullRequestActive.returns( false ); + vi.mocked( isIssueOrPullRequestActive ).mockReturnValue( false ); - expect( isIssueOrPullRequestToClose( issueBase, optionsBase ) ).to.be.true; + expect( isIssueOrPullRequestToClose( issueBase, optionsBase ) ).toEqual( true ); } ); it( 'should return false if issue is not active after stale date and time to close has not passed', () => { optionsBase.closeDate = beforeStaleDate; - stubs.isIssueOrPullRequestActive.returns( false ); + vi.mocked( isIssueOrPullRequestActive ).mockReturnValue( false ); - expect( isIssueOrPullRequestToClose( issueBase, optionsBase ) ).to.be.false; + expect( isIssueOrPullRequestToClose( issueBase, optionsBase ) ).toEqual( false ); } ); it( 'should return false if issue is active after stale date and time to close has passed', () => { optionsBase.closeDate = afterStaleDate; - stubs.isIssueOrPullRequestActive.returns( true ); + vi.mocked( isIssueOrPullRequestActive ).mockReturnValue( true ); - expect( isIssueOrPullRequestToClose( issueBase, optionsBase ) ).to.be.false; + expect( isIssueOrPullRequestToClose( issueBase, optionsBase ) ).toEqual( false ); } ); it( 'should return false if issue is active after stale date and time to close has not passed', () => { optionsBase.closeDate = beforeStaleDate; - stubs.isIssueOrPullRequestActive.returns( true ); + vi.mocked( isIssueOrPullRequestActive ).mockReturnValue( true ); - expect( isIssueOrPullRequestToClose( issueBase, optionsBase ) ).to.be.false; + expect( isIssueOrPullRequestToClose( issueBase, optionsBase ) ).toEqual( false ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttostale.js b/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttostale.js index 36a6bd09b..7d5821801 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttostale.js +++ b/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttostale.js @@ -3,13 +3,15 @@ * For licensing, see LICENSE.md. */ -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import isIssueOrPullRequestActive from '../../lib/utils/isissueorpullrequestactive.js'; +import isIssueOrPullRequestToStale from '../../lib/utils/isissueorpullrequesttostale.js'; + +vi.mock( '../../lib/utils/isissueorpullrequestactive' ); describe( 'dev-stale-bot/lib/utils', () => { describe( 'isIssueOrPullRequestToStale', () => { - let isIssueOrPullRequestToStale, staleDate, issueBase, optionsBase, stubs; + let staleDate, issueBase, optionsBase; beforeEach( () => { staleDate = '2022-12-01T00:00:00Z'; @@ -19,39 +21,29 @@ describe( 'dev-stale-bot/lib/utils', () => { optionsBase = { staleDate }; - - stubs = { - isIssueOrPullRequestActive: sinon.stub() - }; - - isIssueOrPullRequestToStale = proxyquire( '../../lib/utils/isissueorpullrequesttostale', { - './isissueorpullrequestactive': stubs.isIssueOrPullRequestActive - } ); } ); it( 'should be a function', () => { - expect( isIssueOrPullRequestToStale ).to.be.a( 'function' ); + expect( isIssueOrPullRequestToStale ).toBeInstanceOf( Function ); } ); it( 'should check issue activity', () => { isIssueOrPullRequestToStale( issueBase, optionsBase ); - expect( stubs.isIssueOrPullRequestActive.calledOnce ).to.equal( true ); - expect( stubs.isIssueOrPullRequestActive.getCall( 0 ).args[ 0 ] ).to.equal( issueBase ); - expect( stubs.isIssueOrPullRequestActive.getCall( 0 ).args[ 1 ] ).to.equal( staleDate ); - expect( stubs.isIssueOrPullRequestActive.getCall( 0 ).args[ 2 ] ).to.equal( optionsBase ); + expect( vi.mocked( isIssueOrPullRequestActive ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( isIssueOrPullRequestActive ) ).toHaveBeenLastCalledWith( issueBase, staleDate, optionsBase ); } ); it( 'should return true if issue is not active after stale date', () => { - stubs.isIssueOrPullRequestActive.returns( false ); + vi.mocked( isIssueOrPullRequestActive ).mockReturnValue( false ); - expect( isIssueOrPullRequestToStale( issueBase, optionsBase ) ).to.be.true; + expect( isIssueOrPullRequestToStale( issueBase, optionsBase ) ).toEqual( true ); } ); it( 'should return false if issue is active after stale date', () => { - stubs.isIssueOrPullRequestActive.returns( true ); + vi.mocked( isIssueOrPullRequestActive ).mockReturnValue( true ); - expect( isIssueOrPullRequestToStale( issueBase, optionsBase ) ).to.be.false; + expect( isIssueOrPullRequestToStale( issueBase, optionsBase ) ).toEqual( false ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttounstale.js b/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttounstale.js index 82ccc69ab..03be2cb2b 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttounstale.js +++ b/packages/ckeditor5-dev-stale-bot/tests/utils/isissueorpullrequesttounstale.js @@ -3,13 +3,17 @@ * For licensing, see LICENSE.md. */ -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import findStaleDate from '../../lib/utils/findstaledate.js'; +import isIssueOrPullRequestActive from '../../lib/utils/isissueorpullrequestactive.js'; +import isIssueOrPullRequestToUnstale from '../../lib/utils/isissueorpullrequesttounstale.js'; + +vi.mock( '../../lib/utils/findstaledate' ); +vi.mock( '../../lib/utils/isissueorpullrequestactive' ); describe( 'dev-stale-bot/lib/utils', () => { describe( 'isIssueOrPullRequestToUnstale', () => { - let isIssueOrPullRequestToUnstale, staleDate, issueBase, optionsBase, stubs; + let staleDate, issueBase, optionsBase; beforeEach( () => { staleDate = '2022-12-01T00:00:00Z'; @@ -18,48 +22,37 @@ describe( 'dev-stale-bot/lib/utils', () => { optionsBase = {}; - stubs = { - findStaleDate: sinon.stub().returns( staleDate ), - isIssueOrPullRequestActive: sinon.stub() - }; - - isIssueOrPullRequestToUnstale = proxyquire( '../../lib/utils/isissueorpullrequesttounstale', { - './findstaledate': stubs.findStaleDate, - './isissueorpullrequestactive': stubs.isIssueOrPullRequestActive - } ); + vi.mocked( findStaleDate ).mockReturnValue( staleDate ); } ); it( 'should be a function', () => { - expect( isIssueOrPullRequestToUnstale ).to.be.a( 'function' ); + expect( isIssueOrPullRequestToUnstale ).toBeInstanceOf( Function ); } ); it( 'should get the stale date from issue activity', () => { isIssueOrPullRequestToUnstale( issueBase, optionsBase ); - expect( stubs.findStaleDate.calledOnce ).to.equal( true ); - expect( stubs.findStaleDate.getCall( 0 ).args[ 0 ] ).to.equal( issueBase ); - expect( stubs.findStaleDate.getCall( 0 ).args[ 1 ] ).to.equal( optionsBase ); + expect( vi.mocked( findStaleDate ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( findStaleDate ) ).toHaveBeenCalledWith( issueBase, optionsBase ); } ); it( 'should check issue activity', () => { isIssueOrPullRequestToUnstale( issueBase, optionsBase ); - expect( stubs.isIssueOrPullRequestActive.calledOnce ).to.equal( true ); - expect( stubs.isIssueOrPullRequestActive.getCall( 0 ).args[ 0 ] ).to.equal( issueBase ); - expect( stubs.isIssueOrPullRequestActive.getCall( 0 ).args[ 1 ] ).to.equal( staleDate ); - expect( stubs.isIssueOrPullRequestActive.getCall( 0 ).args[ 2 ] ).to.equal( optionsBase ); + expect( vi.mocked( isIssueOrPullRequestActive ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( isIssueOrPullRequestActive ) ).toHaveBeenCalledWith( issueBase, staleDate, optionsBase ); } ); it( 'should return true if issue is active after stale date', () => { - stubs.isIssueOrPullRequestActive.returns( true ); + vi.mocked( isIssueOrPullRequestActive ).mockReturnValue( true ); - expect( isIssueOrPullRequestToUnstale( issueBase, optionsBase ) ).to.be.true; + expect( isIssueOrPullRequestToUnstale( issueBase, optionsBase ) ).toEqual( true ); } ); it( 'should return false if issue is active after stale date', () => { - stubs.isIssueOrPullRequestActive.returns( false ); + vi.mocked( isIssueOrPullRequestActive ).mockReturnValue( false ); - expect( isIssueOrPullRequestToUnstale( issueBase, optionsBase ) ).to.be.false; + expect( isIssueOrPullRequestToUnstale( issueBase, optionsBase ) ).toEqual( false ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuestale.js b/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuestale.js index 7dad5db54..455121f47 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuestale.js +++ b/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuestale.js @@ -3,13 +3,13 @@ * For licensing, see LICENSE.md. */ -const expect = require( 'chai' ).expect; -const isPendingIssueStale = require( '../../lib/utils/ispendingissuestale' ); +import { describe, it, expect } from 'vitest'; +import isPendingIssueStale from '../../lib/utils/ispendingissuestale.js'; describe( 'dev-stale-bot/lib/utils', () => { describe( 'isPendingIssueStale', () => { it( 'should be a function', () => { - expect( isPendingIssueStale ).to.be.a( 'function' ); + expect( isPendingIssueStale ).toBeInstanceOf( Function ); } ); it( 'should return false if issue does not have any label', () => { @@ -20,7 +20,7 @@ describe( 'dev-stale-bot/lib/utils', () => { staleLabels: [ 'pending:feedback' ] }; - expect( isPendingIssueStale( issue, options ) ).to.be.false; + expect( isPendingIssueStale( issue, options ) ).toEqual( false ); } ); it( 'should return false if issue does not have a pending label', () => { @@ -31,7 +31,7 @@ describe( 'dev-stale-bot/lib/utils', () => { staleLabels: [ 'pending:feedback' ] }; - expect( isPendingIssueStale( issue, options ) ).to.be.false; + expect( isPendingIssueStale( issue, options ) ).toEqual( false ); } ); it( 'should return false if issue does not have all pending labels', () => { @@ -42,7 +42,7 @@ describe( 'dev-stale-bot/lib/utils', () => { staleLabels: [ 'pending:feedback', 'pending:even-more-feedback' ] }; - expect( isPendingIssueStale( issue, options ) ).to.be.false; + expect( isPendingIssueStale( issue, options ) ).toEqual( false ); } ); it( 'should return true if issue have all pending labels - single label', () => { @@ -53,7 +53,7 @@ describe( 'dev-stale-bot/lib/utils', () => { staleLabels: [ 'pending:feedback' ] }; - expect( isPendingIssueStale( issue, options ) ).to.be.true; + expect( isPendingIssueStale( issue, options ) ).toEqual( true ); } ); it( 'should return true if issue have all pending labels - multiple labels', () => { @@ -64,7 +64,7 @@ describe( 'dev-stale-bot/lib/utils', () => { staleLabels: [ 'pending:feedback', 'pending:even-more-feedback' ] }; - expect( isPendingIssueStale( issue, options ) ).to.be.true; + expect( isPendingIssueStale( issue, options ) ).toEqual( true ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuetostale.js b/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuetostale.js index 4f506d831..380756baa 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuetostale.js +++ b/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuetostale.js @@ -3,14 +3,15 @@ * For licensing, see LICENSE.md. */ -const expect = require( 'chai' ).expect; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import isPendingIssueToStale from '../../lib/utils/ispendingissuetostale.js'; +import isPendingIssueStale from '../../lib/utils/ispendingissuestale.js'; + +vi.mock( '../../lib/utils/ispendingissuestale' ); describe( 'dev-stale-bot/lib/utils', () => { describe( 'isPendingIssueToStale', () => { - let isPendingIssueToStale, issueBase, optionsBase, stubs; - let staleDatePendingIssue, afterStaleDatePendingIssue, beforeStaleDatePendingIssue; + let issueBase, optionsBase, staleDatePendingIssue, afterStaleDatePendingIssue, beforeStaleDatePendingIssue; beforeEach( () => { staleDatePendingIssue = '2022-12-01T00:00:00Z'; @@ -24,67 +25,58 @@ describe( 'dev-stale-bot/lib/utils', () => { optionsBase = { staleDatePendingIssue }; - - stubs = { - isPendingIssueStale: sinon.stub() - }; - - isPendingIssueToStale = proxyquire( '../../lib/utils/ispendingissuetostale', { - './ispendingissuestale': stubs.isPendingIssueStale - } ); } ); it( 'should be a function', () => { - expect( isPendingIssueToStale ).to.be.a( 'function' ); + expect( isPendingIssueToStale ).toBeInstanceOf( Function ); } ); it( 'should check if issue is stale', () => { isPendingIssueToStale( issueBase, optionsBase ); - expect( stubs.isPendingIssueStale.calledOnce ).to.equal( true ); - expect( stubs.isPendingIssueStale.getCall( 0 ).args[ 0 ] ).to.equal( issueBase ); - expect( stubs.isPendingIssueStale.getCall( 0 ).args[ 1 ] ).to.equal( optionsBase ); + expect( vi.mocked( isPendingIssueStale ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( isPendingIssueStale ) ).toHaveBeenCalledWith( issueBase, optionsBase ); } ); it( 'should return false if issue is already stale', () => { - stubs.isPendingIssueStale.returns( true ); + vi.mocked( isPendingIssueStale ).mockReturnValue( true ); - expect( isPendingIssueToStale( issueBase, optionsBase ) ).to.be.false; + expect( isPendingIssueToStale( issueBase, optionsBase ) ).toEqual( false ); } ); it( 'should return false if issue does not have any comment', () => { - stubs.isPendingIssueStale.returns( false ); + vi.mocked( isPendingIssueStale ).mockReturnValue( false ); - expect( isPendingIssueToStale( issueBase, optionsBase ) ).to.be.false; + expect( isPendingIssueToStale( issueBase, optionsBase ) ).toEqual( false ); } ); it( 'should return false if last comment was created by a community member', () => { - stubs.isPendingIssueStale.returns( false ); + vi.mocked( isPendingIssueStale ).mockReturnValue( false ); issueBase.lastComment = { isExternal: true }; - expect( isPendingIssueToStale( issueBase, optionsBase ) ).to.be.false; + expect( isPendingIssueToStale( issueBase, optionsBase ) ).toEqual( false ); } ); it( 'should return false if last comment was created by a team member and time to stale has not passed', () => { - stubs.isPendingIssueStale.returns( false ); + vi.mocked( isPendingIssueStale ).mockReturnValue( false ); issueBase.lastComment = { isExternal: false, createdAt: afterStaleDatePendingIssue }; - expect( isPendingIssueToStale( issueBase, optionsBase ) ).to.be.false; + expect( isPendingIssueToStale( issueBase, optionsBase ) ).toEqual( false ); } ); it( 'should return true if last comment was created by a team member and time to stale has passed', () => { - stubs.isPendingIssueStale.returns( false ); + vi.mocked( isPendingIssueStale ).mockReturnValue( false ); issueBase.lastComment = { isExternal: false, createdAt: beforeStaleDatePendingIssue }; - expect( isPendingIssueToStale( issueBase, optionsBase ) ).to.be.true; + expect( isPendingIssueToStale( issueBase, optionsBase ) ).toEqual( true ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuetounlabel.js b/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuetounlabel.js index c5676427f..af739f753 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuetounlabel.js +++ b/packages/ckeditor5-dev-stale-bot/tests/utils/ispendingissuetounlabel.js @@ -3,13 +3,13 @@ * For licensing, see LICENSE.md. */ -const expect = require( 'chai' ).expect; -const isPendingIssueToUnlabel = require( '../../lib/utils/ispendingissuetounlabel' ); +import { describe, it, expect } from 'vitest'; +import isPendingIssueToUnlabel from '../../lib/utils/ispendingissuetounlabel.js'; describe( 'dev-stale-bot/lib/utils', () => { describe( 'isPendingIssueToUnlabel', () => { it( 'should be a function', () => { - expect( isPendingIssueToUnlabel ).to.be.a( 'function' ); + expect( isPendingIssueToUnlabel ).toBeInstanceOf( Function ); } ); it( 'should return false if issue does not have any comment', () => { @@ -17,7 +17,7 @@ describe( 'dev-stale-bot/lib/utils', () => { lastComment: null }; - expect( isPendingIssueToUnlabel( issue ) ).to.be.false; + expect( isPendingIssueToUnlabel( issue ) ).toEqual( false ); } ); it( 'should return false if last comment was created by a team member', () => { @@ -27,7 +27,7 @@ describe( 'dev-stale-bot/lib/utils', () => { } }; - expect( isPendingIssueToUnlabel( issue ) ).to.be.false; + expect( isPendingIssueToUnlabel( issue ) ).toEqual( false ); } ); it( 'should return true if last comment was created by a community member', () => { @@ -37,7 +37,7 @@ describe( 'dev-stale-bot/lib/utils', () => { } }; - expect( isPendingIssueToUnlabel( issue ) ).to.be.true; + expect( isPendingIssueToUnlabel( issue ) ).toEqual( true ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-stale-bot/tests/utils/preparesearchquery.js b/packages/ckeditor5-dev-stale-bot/tests/utils/preparesearchquery.js index 0a804bde5..f44b0d0f7 100644 --- a/packages/ckeditor5-dev-stale-bot/tests/utils/preparesearchquery.js +++ b/packages/ckeditor5-dev-stale-bot/tests/utils/preparesearchquery.js @@ -3,45 +3,45 @@ * For licensing, see LICENSE.md. */ -const expect = require( 'chai' ).expect; -const prepareSearchQuery = require( '../../lib/utils/preparesearchquery' ); +import { describe, it, expect } from 'vitest'; +import prepareSearchQuery from '../../lib/utils/preparesearchquery.js'; describe( 'dev-stale-bot/lib/utils', () => { describe( 'prepareSearchQuery', () => { it( 'should be a function', () => { - expect( prepareSearchQuery ).to.be.a( 'function' ); + expect( prepareSearchQuery ).toBeInstanceOf( Function ); } ); it( 'should prepare a query with repository slug', () => { - expect( prepareSearchQuery( { repositorySlug: 'ckeditor/ckeditor5' } ) ).to.include( 'repo:ckeditor/ckeditor5' ); + expect( prepareSearchQuery( { repositorySlug: 'ckeditor/ckeditor5' } ) ).toContain( 'repo:ckeditor/ckeditor5' ); } ); it( 'should prepare a query for issue', () => { - expect( prepareSearchQuery( { type: 'Issue' } ) ).to.include( 'type:issue' ); + expect( prepareSearchQuery( { type: 'Issue' } ) ).toContain( 'type:issue' ); } ); it( 'should prepare a query for pull request', () => { - expect( prepareSearchQuery( { type: 'PullRequest' } ) ).to.include( 'type:pr' ); + expect( prepareSearchQuery( { type: 'PullRequest' } ) ).toContain( 'type:pr' ); } ); it( 'should prepare a query for issue or pull request', () => { - expect( prepareSearchQuery( {} ) ).to.not.include( 'type:' ); + expect( prepareSearchQuery( {} ) ).not.toContain( 'type:' ); } ); it( 'should prepare a query from specified date', () => { - expect( prepareSearchQuery( { searchDate: '2022-12-01' } ) ).to.include( 'created:<2022-12-01' ); + expect( prepareSearchQuery( { searchDate: '2022-12-01' } ) ).toContain( 'created:<2022-12-01' ); } ); it( 'should prepare a query without specifying a start date', () => { - expect( prepareSearchQuery( {} ) ).to.not.include( 'created:' ); + expect( prepareSearchQuery( {} ) ).not.toContain( 'created:' ); } ); it( 'should prepare a query for open items', () => { - expect( prepareSearchQuery( {} ) ).to.include( 'state:open' ); + expect( prepareSearchQuery( {} ) ).toContain( 'state:open' ); } ); it( 'should prepare a query sorted in descending order by creation date', () => { - expect( prepareSearchQuery( {} ) ).to.include( 'sort:created-desc' ); + expect( prepareSearchQuery( {} ) ).toContain( 'sort:created-desc' ); } ); it( 'should prepare a query with ignored labels', () => { @@ -53,7 +53,7 @@ describe( 'dev-stale-bot/lib/utils', () => { 'domain:accessibility' ]; - expect( prepareSearchQuery( { ignoredLabels } ) ).to.include( + expect( prepareSearchQuery( { ignoredLabels } ) ).toContain( '-label:status:stale -label:support:1 -label:support:2 -label:support:3 -label:domain:accessibility' ); } ); @@ -64,7 +64,7 @@ describe( 'dev-stale-bot/lib/utils', () => { 'type:bug' ]; - expect( prepareSearchQuery( { labels } ) ).to.include( 'label:status:stale label:type:bug' ); + expect( prepareSearchQuery( { labels } ) ).toContain( 'label:status:stale label:type:bug' ); } ); it( 'should prepare a query with all fields separated by space', () => { @@ -76,7 +76,7 @@ describe( 'dev-stale-bot/lib/utils', () => { ignoredLabels: [ 'status:stale' ] }; - expect( prepareSearchQuery( options ) ).to.include( + expect( prepareSearchQuery( options ) ).toContain( 'repo:ckeditor/ckeditor5 created:<2022-12-01 type:issue state:open sort:created-desc label:type:bug -label:status:stale' ); } ); diff --git a/packages/ckeditor5-dev-stale-bot/vitest.config.js b/packages/ckeditor5-dev-stale-bot/vitest.config.js new file mode 100644 index 000000000..5ad784a28 --- /dev/null +++ b/packages/ckeditor5-dev-stale-bot/vitest.config.js @@ -0,0 +1,23 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig( { + test: { + testTimeout: 10000, + restoreMocks: true, + include: [ + 'tests/**/*.js' + ], + coverage: { + provider: 'v8', + include: [ + 'lib/**' + ], + reporter: [ 'text', 'json', 'html', 'lcov' ] + } + } +} ); diff --git a/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/README.md b/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/README.md index f86154942..c936ef936 100644 --- a/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/README.md +++ b/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/README.md @@ -18,7 +18,7 @@ yarn run test --files=engine,basic-styles 1. In the IDE, go to _Run_ > _Edit configurations..._: 1. Add a new configuration of type "**Karma**" and a name of your preference. -1. In "Configuration file", selected the "**node\_modules/ckeditor5-dev-tests/bin/intellijkarmarunner/karma.config.js**" file. +1. In "Configuration file", selected the "**node\_modules/ckeditor5-dev-tests/bin/intellijkarmarunner/karma.config.cjs**" file. 1. In "Karma Package", selected the "**node\_modules/ckeditor5-dev-tests/bin/intellijkarmarunner**" directory. 1. In "Karma options", input the CKEditor 5 tests arguments. E.g. `--files=engine,basic-styles`. 1. In "Working directory", select the base `ckeditor5` directory. diff --git a/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/bin/karma b/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/bin/karma index c583d22db..1e9a7c965 100755 --- a/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/bin/karma +++ b/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/bin/karma @@ -33,4 +33,4 @@ const intellijConfig = process.argv.find( item => item.includes( 'intellij.conf. process.argv.push( '--karma-config-overrides=' + intellijConfig ); // Now running the tests. -require( '../../testautomated' ); +import( '../../testautomated.js' ); diff --git a/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/karma.config.js b/packages/ckeditor5-dev-tests/bin/intellijkarmarunner/karma.config.cjs similarity index 100% rename from packages/ckeditor5-dev-tests/bin/intellijkarmarunner/karma.config.js rename to packages/ckeditor5-dev-tests/bin/intellijkarmarunner/karma.config.cjs diff --git a/packages/ckeditor5-dev-tests/bin/postinstall.js b/packages/ckeditor5-dev-tests/bin/postinstall.js index b064e10c0..b206f01e5 100755 --- a/packages/ckeditor5-dev-tests/bin/postinstall.js +++ b/packages/ckeditor5-dev-tests/bin/postinstall.js @@ -5,10 +5,11 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import isWsl from 'is-wsl'; +import { execSync } from 'child_process'; +import { createRequire } from 'module'; -const isWsl = require( 'is-wsl' ); -const { execSync } = require( 'child_process' ); +const require = createRequire( import.meta.url ); if ( isWsl ) { const executables = [ diff --git a/packages/ckeditor5-dev-tests/bin/testautomated.js b/packages/ckeditor5-dev-tests/bin/testautomated.js index 5e745297a..0a71d9fe8 100755 --- a/packages/ckeditor5-dev-tests/bin/testautomated.js +++ b/packages/ckeditor5-dev-tests/bin/testautomated.js @@ -5,11 +5,9 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const chalk = require( 'chalk' ); -const path = require( 'path' ); -const tests = require( '../lib/index' ); +import chalk from 'chalk'; +import path from 'path'; +import * as tests from '../lib/index.js'; const options = tests.parseArguments( process.argv.slice( 2 ) ); diff --git a/packages/ckeditor5-dev-tests/bin/testmanual.js b/packages/ckeditor5-dev-tests/bin/testmanual.js index 62dbb768d..97fd6dbfc 100755 --- a/packages/ckeditor5-dev-tests/bin/testmanual.js +++ b/packages/ckeditor5-dev-tests/bin/testmanual.js @@ -5,11 +5,9 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const chalk = require( 'chalk' ); -const path = require( 'path' ); -const tests = require( '../lib/index' ); +import chalk from 'chalk'; +import path from 'path'; +import * as tests from '../lib/index.js'; const options = tests.parseArguments( process.argv.slice( 2 ) ); @@ -26,5 +24,5 @@ tests.runManualTests( options ) // Mark result of this task as invalid. process.exitCode = 1; - console.log( chalk.red( error ) ); + console.log( chalk.red( error.stack ) ); } ); diff --git a/packages/ckeditor5-dev-tests/lib/index.js b/packages/ckeditor5-dev-tests/lib/index.js index ea0d64110..d519ff94e 100644 --- a/packages/ckeditor5-dev-tests/lib/index.js +++ b/packages/ckeditor5-dev-tests/lib/index.js @@ -3,10 +3,6 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -module.exports = { - runAutomatedTests: require( './tasks/runautomatedtests' ), - runManualTests: require( './tasks/runmanualtests' ), - parseArguments: require( './utils/automated-tests/parsearguments' ) -}; +export { default as runAutomatedTests } from './tasks/runautomatedtests.js'; +export { default as runManualTests } from './tasks/runmanualtests.js'; +export { default as parseArguments } from './utils/automated-tests/parsearguments.js'; diff --git a/packages/ckeditor5-dev-tests/lib/tasks/runautomatedtests.js b/packages/ckeditor5-dev-tests/lib/tasks/runautomatedtests.js index 055f52c9c..a51534de3 100644 --- a/packages/ckeditor5-dev-tests/lib/tasks/runautomatedtests.js +++ b/packages/ckeditor5-dev-tests/lib/tasks/runautomatedtests.js @@ -3,18 +3,21 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const getKarmaConfig = require( '../utils/automated-tests/getkarmaconfig' ); -const chalk = require( 'chalk' ); -const { globSync } = require( 'glob' ); -const minimatch = require( 'minimatch' ); -const mkdirp = require( 'mkdirp' ); -const karmaLogger = require( 'karma/lib/logger.js' ); -const karma = require( 'karma' ); -const transformFileOptionToTestGlob = require( '../utils/transformfileoptiontotestglob' ); +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import getKarmaConfig from '../utils/automated-tests/getkarmaconfig.js'; +import chalk from 'chalk'; +import { globSync } from 'glob'; +import { minimatch } from 'minimatch'; +import { mkdirp } from 'mkdirp'; +import karmaLogger from 'karma/lib/logger.js'; +import karma from 'karma'; +import transformFileOptionToTestGlob from '../utils/transformfileoptiontotestglob.js'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); // Glob patterns that should be ignored. It means if a specified test file is located under path // that matches to these patterns, the file will be skipped. @@ -28,7 +31,7 @@ const IGNORE_GLOBS = [ // An absolute path to the entry file that will be passed to Karma. const ENTRY_FILE_PATH = path.posix.join( process.cwd(), 'build', '.automated-tests', 'entry-point.js' ); -module.exports = function runAutomatedTests( options ) { +export default function runAutomatedTests( options ) { return Promise.resolve().then( () => { if ( !options.production ) { console.warn( chalk.yellow( @@ -48,7 +51,7 @@ module.exports = function runAutomatedTests( options ) { return runKarma( optionsForKarma ); } ); -}; +} function transformFilesToTestGlob( files ) { if ( !Array.isArray( files ) || files.length === 0 ) { @@ -211,7 +214,6 @@ function runKarma( options ) { server.on( 'run_complete', () => { // Use timeout to not write to the console in the middle of Karma's status. setTimeout( () => { - const { logger } = require( '@ckeditor/ckeditor5-dev-utils' ); const log = logger(); log.info( `Coverage report saved in '${ chalk.cyan( coveragePath ) }'.` ); diff --git a/packages/ckeditor5-dev-tests/lib/tasks/runmanualtests.js b/packages/ckeditor5-dev-tests/lib/tasks/runmanualtests.js index 1f8862375..14e0e40ed 100644 --- a/packages/ckeditor5-dev-tests/lib/tasks/runmanualtests.js +++ b/packages/ckeditor5-dev-tests/lib/tasks/runmanualtests.js @@ -3,45 +3,43 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const chalk = require( 'chalk' ); -const { globSync } = require( 'glob' ); -const { spawn } = require( 'child_process' ); -const inquirer = require( 'inquirer' ); -const isInteractive = require( 'is-interactive' ); -const { Server: SocketServer } = require( 'socket.io' ); -const { logger } = require( '@ckeditor/ckeditor5-dev-utils' ); -const createManualTestServer = require( '../utils/manual-tests/createserver' ); -const compileManualTestScripts = require( '../utils/manual-tests/compilescripts' ); -const compileManualTestHtmlFiles = require( '../utils/manual-tests/compilehtmlfiles' ); -const copyAssets = require( '../utils/manual-tests/copyassets' ); -const removeDir = require( '../utils/manual-tests/removedir' ); -const transformFileOptionToTestGlob = require( '../utils/transformfileoptiontotestglob' ); -const requireDll = require( '../utils/requiredll' ); +import fs from 'fs'; +import path from 'path'; +import { spawn } from 'child_process'; +import chalk from 'chalk'; +import { globSync } from 'glob'; +import inquirer from 'inquirer'; +import isInteractive from 'is-interactive'; +import { Server as SocketServer } from 'socket.io'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import createManualTestServer from '../utils/manual-tests/createserver.js'; +import compileManualTestScripts from '../utils/manual-tests/compilescripts.js'; +import compileManualTestHtmlFiles from '../utils/manual-tests/compilehtmlfiles.js'; +import copyAssets from '../utils/manual-tests/copyassets.js'; +import removeDir from '../utils/manual-tests/removedir.js'; +import transformFileOptionToTestGlob from '../utils/transformfileoptiontotestglob.js'; +import requireDll from '../utils/requiredll.js'; /** * Main function that runs manual tests. * - * @param {Object} options - * @param {Array.} options.files Glob patterns specifying which tests to run. - * @param {String} options.themePath A path to the theme the PostCSS theme-importer plugin is supposed to load. - * @param {Boolean} [options.disableWatch=false] Whether to disable the watch mechanism. If set to true, changes in source files + * @param {object} options + * @param {Array.} options.files Glob patterns specifying which tests to run. + * @param {string} options.themePath A path to the theme the PostCSS theme-importer plugin is supposed to load. + * @param {boolean} [options.disableWatch=false] Whether to disable the watch mechanism. If set to true, changes in source files * will not trigger webpack. - * @param {String} [options.language] A language passed to `CKEditorTranslationsPlugin`. - * @param {Array.} [options.additionalLanguages] Additional languages passed to `CKEditorTranslationsPlugin`. - * @param {Number} [options.port] A port number used by the `createManualTestServer`. - * @param {String} [options.identityFile] A file that provides secret keys used in the test scripts. - * @param {String} [options.tsconfig] Path the TypeScript configuration file. - * @param {Boolean|null} [options.dll=null] If `null`, user is asked to create DLL builds, if they are required by test files. + * @param {string} [options.language] A language passed to `CKEditorTranslationsPlugin`. + * @param {Array.} [options.additionalLanguages] Additional languages passed to `CKEditorTranslationsPlugin`. + * @param {number} [options.port] A port number used by the `createManualTestServer`. + * @param {string} [options.identityFile] A file that provides secret keys used in the test scripts. + * @param {string} [options.tsconfig] Path the TypeScript configuration file. + * @param {boolean|null} [options.dll=null] If `null`, user is asked to create DLL builds, if they are required by test files. * If `true`, DLL builds are created automatically, if required by test files. User is not asked. * If `false`, DLL builds are not created. User is not asked. - * @param {Boolean} [options.silent=false] Whether to hide files that will be processed by the script. + * @param {boolean} [options.silent=false] Whether to hide files that will be processed by the script. * @returns {Promise} */ -module.exports = function runManualTests( options ) { +export default function runManualTests( options ) { const log = logger(); const cwd = process.cwd(); const buildDir = path.join( cwd, 'build', '.manual-tests' ); @@ -113,7 +111,7 @@ module.exports = function runManualTests( options ) { /** * Checks if building the DLLs is needed. * - * @param {Array.} sourceFiles + * @param {Array.} sourceFiles * @returns {Promise} */ function isDllBuildRequired( sourceFiles ) { @@ -191,7 +189,7 @@ module.exports = function runManualTests( options ) { /** * Executes the script for building DLLs in the specified repository. * - * @param {String} repositoryPath + * @param {string} repositoryPath * @returns {Promise} */ function buildDllInRepository( repositoryPath ) { @@ -217,4 +215,4 @@ module.exports = function runManualTests( options ) { } ); } ); } -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/assertions/attribute.js b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/assertions/attribute.js index 5976f6b31..acce3f4a5 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/assertions/attribute.js +++ b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/assertions/attribute.js @@ -8,7 +8,7 @@ * * @param {Chai} chai */ -module.exports = chai => { +export default chai => { /** * Asserts that the target has an attribute with the given key name. * @@ -20,8 +20,8 @@ module.exports = chai => { * * Negations works as well. * - * @param {String} key Key of attribute to assert. - * @param {String} [value] Attribute value to assert. + * @param {string} key Key of attribute to assert. + * @param {string} [value] Attribute value to assert. */ chai.Assertion.addMethod( 'attribute', function attributeAssertion( key, value ) { const obj = this._obj; diff --git a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/assertions/equal-markup.js b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/assertions/equal-markup.js index 8d586ea6a..87d7c5ce9 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/assertions/equal-markup.js +++ b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/assertions/equal-markup.js @@ -3,15 +3,15 @@ * For licensing, see LICENSE.md. */ -const AssertionError = require( 'assertion-error' ); -const { html_beautify: beautify } = require( 'js-beautify/js/lib/beautify-html' ); +import { AssertionError } from 'assertion-error'; +import { html_beautify as beautify } from 'js-beautify'; /** * Factory function that registers the `equalMarkup` assertion. * * @param {Chai} chai */ -module.exports = chai => { +export default chai => { /** * Custom assertion that tests whether two given strings containing markup language are equal. * Unlike `expect().to.equal()` form Chai assertion library, this assertion formats the markup before showing a diff. @@ -35,7 +35,7 @@ module.exports = chai => { * '[]foobar' * ); * - * @param {String} expected Markup to compare. + * @param {string} expected Markup to compare. */ chai.Assertion.addMethod( 'equalMarkup', function( expected ) { const actual = this._obj; diff --git a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/getkarmaconfig.js b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/getkarmaconfig.js index 8ae15c36d..1b4b6a68a 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/getkarmaconfig.js +++ b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/getkarmaconfig.js @@ -3,10 +3,14 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import path from 'path'; +import getWebpackConfigForAutomatedTests from './getwebpackconfig.js'; +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; -const path = require( 'path' ); -const getWebpackConfigForAutomatedTests = require( './getwebpackconfig' ); +const require = createRequire( import.meta.url ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); const AVAILABLE_REPORTERS = [ 'mocha', @@ -14,10 +18,10 @@ const AVAILABLE_REPORTERS = [ ]; /** - * @param {Object} options - * @returns {Object} + * @param {object} options + * @returns {object} */ -module.exports = function getKarmaConfig( options ) { +export default function getKarmaConfig( options ) { if ( !AVAILABLE_REPORTERS.includes( options.reporter ) ) { throw new Error( `Specified reporter is not supported. Available reporters: ${ AVAILABLE_REPORTERS.join( ', ' ) }.` ); } @@ -199,7 +203,7 @@ module.exports = function getKarmaConfig( options ) { } return karmaConfig; -}; +} // Returns the value of Karma's browser option. // @returns {Array|null} @@ -219,8 +223,8 @@ function getBrowsers( options ) { // Returns the array of configuration flags for given browser. // -// @param {String} browser -// @returns {Array.} +// @param {string} browser +// @returns {Array.} function getFlagsForBrowser( browser ) { const commonFlags = [ '--disable-background-timer-throttling', diff --git a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/getwebpackconfig.js b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/getwebpackconfig.js index a15cc5770..64fc4e03f 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/getwebpackconfig.js +++ b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/getwebpackconfig.js @@ -3,19 +3,21 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import path from 'path'; +import webpack from 'webpack'; +import { loaders } from '@ckeditor/ckeditor5-dev-utils'; +import getDefinitionsFromFile from '../getdefinitionsfromfile.js'; +import TreatWarningsAsErrorsWebpackPlugin from './treatwarningsaserrorswebpackplugin.js'; +import { fileURLToPath } from 'url'; -const path = require( 'path' ); -const webpack = require( 'webpack' ); -const { loaders } = require( '@ckeditor/ckeditor5-dev-utils' ); -const getDefinitionsFromFile = require( '../getdefinitionsfromfile' ); -const TreatWarningsAsErrorsWebpackPlugin = require( './treatwarningsaserrorswebpackplugin' ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); /** - * @param {Object} options - * @returns {Object} + * @param {object} options + * @returns {object} */ -module.exports = function getWebpackConfigForAutomatedTests( options ) { +export default function getWebpackConfigForAutomatedTests( options ) { const definitions = Object.assign( {}, getDefinitionsFromFile( options.identityFile ) ); const config = { @@ -102,4 +104,4 @@ module.exports = function getWebpackConfigForAutomatedTests( options ) { } return config; -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/karmanotifier.js b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/karmanotifier.js index cfb5a8fde..81a8e100c 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/karmanotifier.js +++ b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/karmanotifier.js @@ -3,8 +3,12 @@ * For licensing, see LICENSE.md. */ -const path = require( 'path' ); -const notifier = require( 'node-notifier' ); +import path from 'path'; +import notifier from 'node-notifier'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); const ckeditor5icon = path.join( __dirname, '..', 'icons', 'ckeditor5.png' ); @@ -14,7 +18,7 @@ const defaultNotifyOptions = { icon: ckeditor5icon }; -module.exports = { 'reporter:karmanotifier': [ 'type', karmaNotifier ] }; +export default { 'reporter:karmanotifier': [ 'type', karmaNotifier ] }; karmaNotifier.$inject = [ 'helper' ]; function karmaNotifier( helper ) { diff --git a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/parsearguments.js b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/parsearguments.js index eefdb774b..26c9cd9e9 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/parsearguments.js +++ b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/parsearguments.js @@ -3,18 +3,16 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const minimist = require( 'minimist' ); -const { tools, logger } = require( '@ckeditor/ckeditor5-dev-utils' ); +import fs from 'fs-extra'; +import path from 'path'; +import minimist from 'minimist'; +import { tools, logger } from '@ckeditor/ckeditor5-dev-utils'; /** - * @param {Array.} args - * @returns {Object} + * @param {Array.} args + * @returns {object} */ -module.exports = function parseArguments( args ) { +export default function parseArguments( args ) { const log = logger(); const minimistConfig = { @@ -117,8 +115,8 @@ module.exports = function parseArguments( args ) { * Replaces all kebab-case keys in the `options` object with camelCase entries. * Kebab-case keys will be removed. * - * @param {Object} options - * @param {Array.} keys Kebab-case keys in `options` object. + * @param {object} options + * @param {Array.} keys Kebab-case keys in `options` object. */ function replaceKebabCaseWithCamelCase( options, keys ) { for ( const key of keys ) { @@ -132,7 +130,7 @@ module.exports = function parseArguments( args ) { /** * Parses the `--debug` option. * - * @param {Object} options + * @param {object} options */ function parseDebugOption( options ) { if ( options.debug === 'false' || options.debug === false ) { @@ -155,7 +153,7 @@ module.exports = function parseArguments( args ) { * * The `ckeditor5-` prefix will be removed. * - * @param {Object} options + * @param {object} options */ function parseRepositoriesOption( options ) { if ( !options.repositories.length ) { @@ -172,7 +170,7 @@ module.exports = function parseArguments( args ) { const files = new Set( options.files ); for ( const repositoryName of options.repositories ) { - const cwdPackageJson = require( path.join( cwd, 'package.json' ) ); + const cwdPackageJson = fs.readJsonSync( path.join( cwd, 'package.json' ) ); // Check the main repository. if ( repositoryName === cwdPackageJson.name ) { @@ -209,7 +207,7 @@ module.exports = function parseArguments( args ) { * Parses the `--tsconfig` options to be an absolute path. If argument is not provided, * it will check if `tsconfig.test.json` file exists and use it if it does. * - * @param {Object} options + * @param {object} options */ function parseTsconfigPath( options ) { if ( options.tsconfig ) { @@ -226,8 +224,8 @@ module.exports = function parseArguments( args ) { /** * Splits by a comma (`,`) all values specified under keys to array. * - * @param {Object} options - * @param {Array.} keys Kebab-case keys in `options` object. + * @param {object} options + * @param {Array.} keys Kebab-case keys in `options` object. */ function splitOptionsToArray( options, keys ) { for ( const key of keys ) { @@ -240,8 +238,8 @@ module.exports = function parseArguments( args ) { /** * Returns a camel case value for specified kebab-case `value`. * - * @param {String} value Kebab-case string. - * @returns {String} + * @param {string} value Kebab-case string. + * @returns {string} */ function toCamelCase( value ) { return value.split( '-' ) @@ -256,8 +254,8 @@ module.exports = function parseArguments( args ) { } /** - * @param {String} path - * @returns {Boolean} + * @param {string} path + * @returns {boolean} */ function isDirectory( path ) { try { @@ -266,4 +264,4 @@ module.exports = function parseArguments( args ) { return false; } } -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/treatwarningsaserrorswebpackplugin.js b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/treatwarningsaserrorswebpackplugin.js index bcd0a1bcc..718680e25 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/automated-tests/treatwarningsaserrorswebpackplugin.js +++ b/packages/ckeditor5-dev-tests/lib/utils/automated-tests/treatwarningsaserrorswebpackplugin.js @@ -6,7 +6,7 @@ /** * Webpack plugin that reassigns warnings as errors and stops the process if any errors or warnings detected. */ -module.exports = class TreatWarningsAsErrorsWebpackPlugin { +export default class TreatWarningsAsErrorsWebpackPlugin { apply( compiler ) { compiler.hooks.shouldEmit.tap( 'TreatWarningsAsErrorsWebpackPlugin', compilation => { compilation.errors = [ @@ -21,4 +21,4 @@ module.exports = class TreatWarningsAsErrorsWebpackPlugin { } } ); } -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/getdefinitionsfromfile.js b/packages/ckeditor5-dev-tests/lib/utils/getdefinitionsfromfile.js index 7729b456a..467b803aa 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/getdefinitionsfromfile.js +++ b/packages/ckeditor5-dev-tests/lib/utils/getdefinitionsfromfile.js @@ -3,15 +3,16 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import path from 'path'; +import { createRequire } from 'module'; -const path = require( 'path' ); +const require = createRequire( import.meta.url ); /** - * @param {String|null} definitionSource - * @returns {Object} + * @param {string|null} definitionSource + * @returns {object} */ -module.exports = function getDefinitionsFromFile( definitionSource ) { +export default function getDefinitionsFromFile( definitionSource ) { if ( !definitionSource ) { return {}; } @@ -31,11 +32,11 @@ module.exports = function getDefinitionsFromFile( definitionSource ) { return {}; } -}; +} /** - * @param {String|null} definitionSource - * @returns {String|null} + * @param {string|null} definitionSource + * @returns {string|null} */ function normalizeDefinitionSource( definitionSource ) { // Passed an absolute path. diff --git a/packages/ckeditor5-dev-tests/lib/utils/getrelativefilepath.js b/packages/ckeditor5-dev-tests/lib/utils/getrelativefilepath.js index 80acee08f..b673735d2 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/getrelativefilepath.js +++ b/packages/ckeditor5-dev-tests/lib/utils/getrelativefilepath.js @@ -3,9 +3,7 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); +import path from 'path'; /** * Get a path to a source file which will uniquely identify this file in @@ -17,11 +15,11 @@ const path = require( 'path' ); * - /work/space/packages/ckeditor5-foo/tests/manual/foo.js -> ckeditor5-foo/tests/manual/foo.js * - /work/space/packages/ckeditor-foo/tests/manual/foo.js -> ckeditor-foo/tests/manual/foo.js * - * @param {String} filePath - * @param {String} [cwd=process.cwd()] - * @returns {String} + * @param {string} filePath + * @param {string} [cwd=process.cwd()] + * @returns {string} */ -module.exports = function getRelativeFilePath( filePath, cwd = process.cwd() ) { +export default function getRelativeFilePath( filePath, cwd = process.cwd() ) { // The path ends with the directory separator. const relativePath = filePath.replace( cwd, '' ).slice( 1 ); @@ -32,4 +30,4 @@ module.exports = function getRelativeFilePath( filePath, cwd = process.cwd() ) { // The main repository. return path.join( 'ckeditor5', relativePath ); -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/compilehtmlfiles.js b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/compilehtmlfiles.js index 796d41fed..57a8d684a 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/compilehtmlfiles.js +++ b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/compilehtmlfiles.js @@ -3,34 +3,36 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const fs = require( 'fs-extra' ); -const { globSync } = require( 'glob' ); -const _ = require( 'lodash' ); -const chalk = require( 'chalk' ); -const commonmark = require( 'commonmark' ); -const combine = require( 'dom-combiner' ); -const chokidar = require( 'chokidar' ); -const { logger } = require( '@ckeditor/ckeditor5-dev-utils' ); -const getRelativeFilePath = require( '../getrelativefilepath' ); +import path from 'path'; +import fs from 'fs-extra'; +import { globSync } from 'glob'; +import { uniq, debounce } from 'lodash-es'; +import chalk from 'chalk'; +import * as commonmark from 'commonmark'; +import combine from 'dom-combiner'; +import chokidar from 'chokidar'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import getRelativeFilePath from '../getrelativefilepath.js'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); const reader = new commonmark.Parser(); const writer = new commonmark.HtmlRenderer(); /** - * @param {Object} options - * @param {String} options.buildDir A path where compiled files will be saved. - * @param {Array.} options.sourceFiles An array of paths to JavaScript files from manual tests to be compiled. - * @param {String} options.language A language passed to `CKEditorTranslationsPlugin`. - * @param {Boolean} options.disableWatch Whether to disable the watch mechanism. If set to true, changes in source files + * @param {object} options + * @param {string} options.buildDir A path where compiled files will be saved. + * @param {Array.} options.sourceFiles An array of paths to JavaScript files from manual tests to be compiled. + * @param {string} options.language A language passed to `CKEditorTranslationsPlugin`. + * @param {boolean} options.disableWatch Whether to disable the watch mechanism. If set to true, changes in source files * will not trigger webpack. - * @param {Array.} [options.additionalLanguages] Additional languages passed to `CKEditorTranslationsPlugin`. - * @param {Boolean} [options.silent=false] Whether to hide files that will be processed by the script. + * @param {Array.} [options.additionalLanguages] Additional languages passed to `CKEditorTranslationsPlugin`. + * @param {boolean} [options.silent=false] Whether to hide files that will be processed by the script. * @returns {Promise} */ -module.exports = function compileHtmlFiles( options ) { +export default function compileHtmlFiles( options ) { const buildDir = options.buildDir; const viewTemplate = fs.readFileSync( path.join( __dirname, 'template.html' ), 'utf-8' ); const silent = options.silent || false; @@ -38,12 +40,12 @@ module.exports = function compileHtmlFiles( options ) { const sourceMDFiles = options.sourceFiles.map( jsFile => setExtension( jsFile, 'md' ) ); const sourceHtmlFiles = sourceMDFiles.map( mdFile => setExtension( mdFile, 'html' ) ); - const sourceDirs = _.uniq( sourceMDFiles.map( file => path.dirname( file ) ) ); + const sourceDirs = uniq( sourceMDFiles.map( file => path.dirname( file ) ) ); const sourceFilePathBases = sourceMDFiles.map( mdFile => getFilePathWithoutExtension( mdFile ) ); const staticFiles = sourceDirs .flatMap( sourceDir => { - const globPattern = path.join( sourceDir, '**', '*.!(js|html|md)' ).split( path.sep ).join( '/' ); + const globPattern = path.join( sourceDir, '**', '*.!(js|html|md)' ).split( /[\\/]/ ).join( '/' ); return globSync( globPattern ); } ) @@ -80,15 +82,15 @@ module.exports = function compileHtmlFiles( options ) { } ); }, options.onTestCompilationStatus ); } -}; +} /** - * @param {String} buildDir An absolute path to the directory where the processed file should be saved. - * @param {Object} options - * @param {String} options.filePath An absolute path to the manual test assets without the extension. - * @param {String} options.template The HTML template which will be merged with the manual test HTML file. - * @param {Array.} options.languages Name of translations that should be added to the manual test. - * @param {Boolean} options.silent Whether to hide files that will be processed by the script. + * @param {string} buildDir An absolute path to the directory where the processed file should be saved. + * @param {object} options + * @param {string} options.filePath An absolute path to the manual test assets without the extension. + * @param {string} options.template The HTML template which will be merged with the manual test HTML file. + * @param {Array.} options.languages Name of translations that should be added to the manual test. + * @param {boolean} options.silent Whether to hide files that will be processed by the script. */ function compileHtmlFile( buildDir, options ) { const sourceFilePathBase = options.filePath; @@ -181,7 +183,7 @@ function getFilePathWithoutExtension( file ) { function watchFiles( filePaths, onChange, onTestCompilationStatus ) { for ( const filePath of filePaths ) { - const debouncedOnChange = _.debounce( () => { + const debouncedOnChange = debounce( () => { onChange( filePath ); onTestCompilationStatus( 'finished' ); }, 500 ); diff --git a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/compilescripts.js b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/compilescripts.js index 4853448e4..0d204fe67 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/compilescripts.js +++ b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/compilescripts.js @@ -3,29 +3,27 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const webpack = require( 'webpack' ); -const getWebpackConfigForManualTests = require( './getwebpackconfig' ); -const getRelativeFilePath = require( '../getrelativefilepath' ); -const requireDll = require( '../requiredll' ); +import webpack from 'webpack'; +import getWebpackConfigForManualTests from './getwebpackconfig.js'; +import getRelativeFilePath from '../getrelativefilepath.js'; +import requireDll from '../requiredll.js'; /** - * @param {Object} options - * @param {String} options.cwd Current working directory. Usually it points to the CKEditor 5 root directory. - * @param {String} options.buildDir A path where compiled files will be saved. - * @param {Array.} options.sourceFiles An array of paths to JavaScript files from manual tests to be compiled. - * @param {String} options.themePath A path to the theme the PostCSS theme-importer plugin is supposed to load. - * @param {String} options.language A language passed to `CKEditorTranslationsPlugin`. - * @param {Boolean} options.disableWatch Whether to disable the watch mechanism. If set to true, changes in source files + * @param {object} options + * @param {string} options.cwd Current working directory. Usually it points to the CKEditor 5 root directory. + * @param {string} options.buildDir A path where compiled files will be saved. + * @param {Array.} options.sourceFiles An array of paths to JavaScript files from manual tests to be compiled. + * @param {string} options.themePath A path to the theme the PostCSS theme-importer plugin is supposed to load. + * @param {string} options.language A language passed to `CKEditorTranslationsPlugin`. + * @param {boolean} options.disableWatch Whether to disable the watch mechanism. If set to true, changes in source files * will not trigger webpack. - * @param {Function} options.onTestCompilationStatus A callback called whenever the script compilation occurrs. - * @param {String} [options.tsconfig] Path the TypeScript configuration file. - * @param {Array.} [options.additionalLanguages] Additional languages passed to `CKEditorTranslationsPlugin`. - * @param {String} [options.identityFile] A file that provides secret keys used in the test scripts. + * @param {function} options.onTestCompilationStatus A callback called whenever the script compilation occurrs. + * @param {string} [options.tsconfig] Path the TypeScript configuration file. + * @param {Array.} [options.additionalLanguages] Additional languages passed to `CKEditorTranslationsPlugin`. + * @param {string} [options.identityFile] A file that provides secret keys used in the test scripts. * @returns {Promise} */ -module.exports = function compileManualTestScripts( options ) { +export default function compileManualTestScripts( options ) { const entryFiles = options.sourceFiles; const entryFilesDLL = entryFiles.filter( entryFile => requireDll( entryFile ) ); const entryFilesNonDll = entryFiles.filter( entryFile => !requireDll( entryFile ) ); @@ -71,7 +69,7 @@ module.exports = function compileManualTestScripts( options ) { const webpackPromises = webpackConfigs.map( config => runWebpack( config ) ); return Promise.all( webpackPromises ); -}; +} /** * @returns {Promise} diff --git a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/copyassets.js b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/copyassets.js index 36c583200..75d71d3a1 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/copyassets.js +++ b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/copyassets.js @@ -3,8 +3,14 @@ * For licensing, see LICENSE.md. */ -const path = require( 'path' ); -const fs = require( 'fs-extra' ); +import path from 'path'; +import fs from 'fs-extra'; +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; + +const require = createRequire( import.meta.url ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); const assets = [ path.join( __dirname, 'togglesidebar.js' ), @@ -23,9 +29,9 @@ const assets = [ * │ └── ... * ... */ -module.exports = function copyAssets( buildDir ) { +export default function copyAssets( buildDir ) { for ( const assetPath of assets ) { const outputFilePath = path.join( buildDir, 'assets', path.basename( assetPath ) ); fs.copySync( assetPath, outputFilePath ); } -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/createserver.js b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/createserver.js index 29460b290..429b1d1b8 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/createserver.js +++ b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/createserver.js @@ -3,40 +3,40 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const http = require( 'http' ); -const path = require( 'path' ); -const { globSync } = require( 'glob' ); -const fs = require( 'fs' ); -const combine = require( 'dom-combiner' ); -const { logger } = require( '@ckeditor/ckeditor5-dev-utils' ); +import http from 'http'; +import fs from 'fs'; +import path from 'path'; +import readline from 'readline'; +import { fileURLToPath } from 'url'; +import { globSync } from 'glob'; +import combine from 'dom-combiner'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); /** * Basic HTTP server. * - * @param {String} sourcePath Base path where the compiler saved the files. - * @param {Number} [port=8125] Port to listen at. - * @param {Function} [onCreate] A callback called with the reference to the HTTP server when it is up and running. + * @param {string} sourcePath Base path where the compiler saved the files. + * @param {number} [port=8125] Port to listen at. + * @param {function} [onCreate] A callback called with the reference to the HTTP server when it is up and running. */ -module.exports = function createManualTestServer( sourcePath, port = 8125, onCreate ) { +export default function createManualTestServer( sourcePath, port = 8125, onCreate ) { return new Promise( resolve => { const server = http.createServer( ( request, response ) => { onRequest( sourcePath, request, response ); } ).listen( port ); - // SIGINT isn't caught on Windows in process. However CTRL+C can be catch + // SIGINT isn't caught on Windows in process. However, `CTRL+C` can be caught // by `readline` module. After that we can emit SIGINT to the process manually. if ( process.platform === 'win32' ) { - const readline = require( 'readline' ).createInterface( { + const readlineInterface = readline.createInterface( { input: process.stdin, output: process.stdout } ); - // Save the reference of the stream to be able to close it in tests. - server._readline = readline; - - readline.on( 'SIGINT', () => { + readlineInterface.on( 'SIGINT', () => { process.emit( 'SIGINT' ); } ); } @@ -56,20 +56,22 @@ module.exports = function createManualTestServer( sourcePath, port = 8125, onCre onCreate( server ); } } ); -}; +} function onRequest( sourcePath, request, response ) { - response.writeHead( 200, { - 'Content-Type': getContentType( request.url.endsWith( '/' ) ? '.html' : path.extname( request.url ) ) - } ); + const contentType = getContentType( request.url.endsWith( '/' ) ? '.html' : path.extname( request.url ) ); // Ignore a 'favicon' request. if ( request.url === '/favicon.ico' ) { + response.writeHead( 200, { 'Content-Type': contentType } ); + return response.end( null, 'utf-8' ); } // Generate index.html with list of the tests. if ( request.url === '/' ) { + response.writeHead( 200, { 'Content-Type': contentType } ); + return response.end( generateIndex( sourcePath ), 'utf-8' ); } @@ -79,7 +81,9 @@ function onRequest( sourcePath, request, response ) { const url = request.url.replace( /\?.+$/, '' ); const content = fs.readFileSync( path.join( sourcePath, url ) ); - response.end( content, 'utf-8' ); + response.writeHead( 200, { 'Content-Type': contentType } ); + + return response.end( content, 'utf-8' ); } catch ( error ) { logger().error( `[Server] Cannot find file '${ request.url }'.` ); @@ -90,8 +94,8 @@ function onRequest( sourcePath, request, response ) { // Returns content type based on file extension. // -// @params {String} fileExtension -// @returns {String} +// @params {string} fileExtension +// @returns {string} function getContentType( fileExtension ) { switch ( fileExtension ) { case '.js': @@ -101,6 +105,7 @@ function getContentType( fileExtension ) { return 'text/css'; case '.json': + case '.map': return 'application/json'; case '.png': @@ -119,8 +124,8 @@ function getContentType( fileExtension ) { // Generates a list with available manual tests. // -// @param {String} sourcePath Base path that will be used to resolve all patterns. -// @returns {String} +// @param {string} sourcePath Base path that will be used to resolve all patterns. +// @returns {string} function generateIndex( sourcePath ) { const viewTemplate = fs.readFileSync( path.join( __dirname, 'template.html' ), 'utf-8' ); const globPattern = path.join( sourcePath, '**', '*.html' ).replace( /\\/g, '/' ); @@ -152,6 +157,8 @@ function generateIndex( sourcePath ) { testList += ''; } + testList += ''; + const headerHtml = '

CKEditor 5 manual tests

'; return combine( viewTemplate, headerHtml, testList ); diff --git a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/getwebpackconfig.js b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/getwebpackconfig.js index 4dbab390b..913121759 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/getwebpackconfig.js +++ b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/getwebpackconfig.js @@ -3,30 +3,35 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +import webpack from 'webpack'; +import { CKEditorTranslationsPlugin } from '@ckeditor/ckeditor5-dev-translations'; +import { loaders } from '@ckeditor/ckeditor5-dev-utils'; +import WebpackNotifierPlugin from './webpacknotifierplugin.js'; +import getDefinitionsFromFile from '../getdefinitionsfromfile.js'; -const path = require( 'path' ); -const webpack = require( 'webpack' ); -const { CKEditorTranslationsPlugin } = require( '@ckeditor/ckeditor5-dev-translations' ); -const { loaders } = require( '@ckeditor/ckeditor5-dev-utils' ); -const WebpackNotifierPlugin = require( './webpacknotifierplugin' ); -const getDefinitionsFromFile = require( '../getdefinitionsfromfile' ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); + +const require = createRequire( import.meta.url ); /** - * @param {Object} options - * @param {String} options.cwd Current working directory. Usually it points to the CKEditor 5 root directory. - * @param {Boolean} options.requireDll A flag describing whether DLL builds are required for starting the manual test server. - * @param {Object} options.entries - * @param {String} options.buildDir - * @param {String} options.themePath - * @param {Boolean} options.disableWatch - * @param {String} [options.tsconfig] - * @param {String} [options.language] - * @param {Array.} [options.additionalLanguages] - * @param {String|null} [options.identityFile] - * @returns {Object} + * @param {object} options + * @param {string} options.cwd Current working directory. Usually it points to the CKEditor 5 root directory. + * @param {boolean} options.requireDll A flag describing whether DLL builds are required for starting the manual test server. + * @param {object} options.entries + * @param {string} options.buildDir + * @param {string} options.themePath + * @param {boolean} options.disableWatch + * @param {string} [options.tsconfig] + * @param {string} [options.language] + * @param {Array.} [options.additionalLanguages] + * @param {string|null} [options.identityFile] + * @returns {object} */ -module.exports = function getWebpackConfigForManualTests( options ) { +export default function getWebpackConfigForManualTests( options ) { const definitions = Object.assign( {}, getDefinitionsFromFile( options.identityFile ) ); const webpackConfig = { @@ -149,5 +154,5 @@ module.exports = function getWebpackConfigForManualTests( options ) { } return webpackConfig; -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/removedir.js b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/removedir.js index 069926fcd..b5b1186b5 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/removedir.js +++ b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/removedir.js @@ -3,28 +3,26 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const del = require( 'del' ); -const { logger } = require( '@ckeditor/ckeditor5-dev-utils' ); -const chalk = require( 'chalk' ); +import { deleteAsync } from 'del'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import chalk from 'chalk'; /** * Removes the specified directory. * * The `del` package protects you against deleting the current working directory and above. * - * @param {String} dir Directory to remove. - * @param {Object} [options={}] options - * @param {Boolean} [options.silent=false] Whether to hide the path to the directory on the console. + * @param {string} dir Directory to remove. + * @param {object} [options={}] options + * @param {boolean} [options.silent=false] Whether to hide the path to the directory on the console. * @returns {Promise} */ -module.exports = function removeDir( dir, options = {} ) { - return del( dir ).then( () => { +export default function removeDir( dir, options = {} ) { + return deleteAsync( dir ).then( () => { if ( !options.silent ) { logger().info( `Removed directory '${ chalk.cyan( dir ) }'` ); } return Promise.resolve(); } ); -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/webpacknotifierplugin.js b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/webpacknotifierplugin.js index 01d1f3b96..e6f4eb4d0 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/manual-tests/webpacknotifierplugin.js +++ b/packages/ckeditor5-dev-tests/lib/utils/manual-tests/webpacknotifierplugin.js @@ -3,18 +3,16 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { logger } = require( '@ckeditor/ckeditor5-dev-utils' ); +import { logger } from '@ckeditor/ckeditor5-dev-utils'; /** * Plugin for Webpack which helps to inform the developer about processes. */ -module.exports = class WebpackNotifierPlugin { +export default class WebpackNotifierPlugin { /** - * @param {Object} options - * @param {Function} options.onTestCompilationStatus - * @param {String} options.processName + * @param {object} options + * @param {function} options.onTestCompilationStatus + * @param {string} options.processName */ constructor( options ) { this.log = logger(); @@ -56,4 +54,4 @@ module.exports = class WebpackNotifierPlugin { this.onTestCompilationStatus( `finished:${ this.processName }` ); } ); } -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/requiredll.js b/packages/ckeditor5-dev-tests/lib/utils/requiredll.js index 023064783..ec8b69144 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/requiredll.js +++ b/packages/ckeditor5-dev-tests/lib/utils/requiredll.js @@ -3,16 +3,14 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /** * Returns `true` if any of the source files represent a DLL test. * - * @param {String|Array.} sourceFiles - * @returns {Boolean} + * @param {string|Array.} sourceFiles + * @returns {boolean} */ -module.exports = function requireDll( sourceFiles ) { +export default function requireDll( sourceFiles ) { sourceFiles = Array.isArray( sourceFiles ) ? sourceFiles : [ sourceFiles ]; return sourceFiles.some( filePath => /-dll.[jt]s$/.test( filePath ) ); -}; +} diff --git a/packages/ckeditor5-dev-tests/lib/utils/transformfileoptiontotestglob.js b/packages/ckeditor5-dev-tests/lib/utils/transformfileoptiontotestglob.js index e8382c368..62faf1625 100644 --- a/packages/ckeditor5-dev-tests/lib/utils/transformfileoptiontotestglob.js +++ b/packages/ckeditor5-dev-tests/lib/utils/transformfileoptiontotestglob.js @@ -3,12 +3,10 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import fs from 'fs'; +import path from 'path'; -const fs = require( 'fs' ); -const path = require( 'path' ); - -const EXTERNAL_DIR_PATH = path.join( process.cwd(), 'external' ); +const EXTERNAL_DIR_NAME = 'external'; /** * Converts values of `--files` argument to proper globs. Handles both JS and TS files. These are the supported types of values: @@ -20,11 +18,11 @@ const EXTERNAL_DIR_PATH = path.join( process.cwd(), 'external' ); * * "foo/bar/" - matches all tests from a package and a subdirectory. * * "foo/bar" - matches all tests from a package (or root) with specific filename. * - * @param {String} pattern A path or pattern to determine the tests to execute. - * @param {Boolean} [isManualTest=false] Whether the tests are manual or automated. - * @returns {Array.} + * @param {string} pattern A path or pattern to determine the tests to execute. + * @param {boolean} [isManualTest=false] Whether the tests are manual or automated. + * @returns {Array.} */ -module.exports = function transformFileOptionToTestGlob( pattern, isManualTest = false ) { +export default function transformFileOptionToTestGlob( pattern, isManualTest = false ) { if ( doesPatternMatchExternalRepositoryName( pattern ) ) { return getExternalRepositoryGlob( pattern, { isManualTest } ); } @@ -54,18 +52,20 @@ module.exports = function transformFileOptionToTestGlob( pattern, isManualTest = transformedPathForExternalPackagesWithCKEditorPrefix ] ) ]; -}; +} /** - * @param {String} pattern - * @param {Object} [options] - * @param {Boolean} [options.isManualTest] Controlls the path for manual and automated tests. - * @returns {Array.} + * @param {string} pattern + * @param {object} [options] + * @param {boolean} [options.isManualTest] Controlls the path for manual and automated tests. + * @returns {Array.} */ function getExternalRepositoryGlob( pattern, { isManualTest } ) { + const externalPath = path.join( process.cwd(), EXTERNAL_DIR_NAME ); + const repositoryGlob = isManualTest ? - path.join( EXTERNAL_DIR_PATH, pattern, 'tests', 'manual', '**', '*' ) + '.{js,ts}' : - path.join( EXTERNAL_DIR_PATH, pattern, 'tests', '**', '*' ) + '.{js,ts}'; + path.join( externalPath, pattern, 'tests', 'manual', '**', '*' ) + '.{js,ts}' : + path.join( externalPath, pattern, 'tests', '**', '*' ) + '.{js,ts}'; return [ repositoryGlob.split( path.sep ).join( path.posix.sep ) @@ -73,26 +73,28 @@ function getExternalRepositoryGlob( pattern, { isManualTest } ) { } /** - * @param {String} pattern - * @returns {Boolean} + * @param {string} pattern + * @returns {boolean} */ function doesPatternMatchExternalRepositoryName( pattern ) { - if ( !fs.existsSync( EXTERNAL_DIR_PATH ) ) { + const externalPath = path.join( process.cwd(), EXTERNAL_DIR_NAME ); + + if ( !fs.existsSync( externalPath ) ) { return false; } - return fs.readdirSync( EXTERNAL_DIR_PATH ) - .filter( externalDir => fs.statSync( path.join( EXTERNAL_DIR_PATH, externalDir ) ).isDirectory() ) + return fs.readdirSync( externalPath ) + .filter( externalDir => fs.statSync( path.join( externalPath, externalDir ) ).isDirectory() ) .includes( pattern ); } /** - * @param {String} pattern - * @param {Object} [options={}] - * @param {Boolean} [options.isManualTest=false] Whether the tests are manual or automated. - * @param {Boolean} [options.useCKEditorPrefix=false] If true, the returned path will use 'ckeditor' prefix instead of 'ckeditor5'. - * @param {Boolean} [options.externalPackages] If true, the returned path will contain "external\/**\/packages". - * @returns {String} + * @param {string} pattern + * @param {object} [options={}] + * @param {boolean} [options.isManualTest=false] Whether the tests are manual or automated. + * @param {boolean} [options.useCKEditorPrefix=false] If true, the returned path will use 'ckeditor' prefix instead of 'ckeditor5'. + * @param {boolean} [options.externalPackages] If true, the returned path will contain "external\/**\/packages". + * @returns {string} */ function transformSinglePattern( pattern, options ) { const chunks = pattern.match( /[a-z1-9|*-]+/g ); diff --git a/packages/ckeditor5-dev-tests/package.json b/packages/ckeditor5-dev-tests/package.json index 8378830b1..54472b136 100644 --- a/packages/ckeditor5-dev-tests/package.json +++ b/packages/ckeditor5-dev-tests/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-tests", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "Testing environment for CKEditor 5.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -16,6 +16,7 @@ "node": ">=18.0.0", "npm": ">=5.7.1" }, + "type": "module", "main": "lib/index.js", "files": [ "lib", @@ -27,55 +28,60 @@ }, "dependencies": { "@babel/core": "^7.10.5", - "@ckeditor/ckeditor5-dev-translations": "^43.0.0", - "@ckeditor/ckeditor5-dev-utils": "^43.0.0", + "@ckeditor/ckeditor5-dev-translations": "^44.0.0-alpha.5", + "@ckeditor/ckeditor5-dev-utils": "^44.0.0-alpha.5", "@ckeditor/ckeditor5-inspector": "^4.0.0", "@types/chai": "^4.3.5", "@types/karma-sinon-chai": "^2.0.2", "@types/mocha": "^10.0.1", "@types/sinon": "^10.0.15", - "assertion-error": "^1.1.0", - "babel-plugin-istanbul": "^6.1.0", + "assertion-error": "^2.0.0", + "babel-plugin-istanbul": "^7.0.0", "buffer": "^6.0.3", - "chai": "^4.2.0", - "chalk": "^4.0.0", - "chokidar": "^3.4.0", + "chai": "^4.5.0", + "chalk": "^5.0.0", + "chokidar": "^4.0.0", "commonmark": "^0.29.1", - "del": "^5.1.0", + "del": "^7.0.0", "dom-combiner": "^0.1.3", - "fs-extra": "^11.2.0", - "glob": "^10.2.5", - "inquirer": "^7.1.0", - "is-interactive": "^1.0.0", - "is-wsl": "^2.2.0", + "fs-extra": "^11.0.0", + "glob": "^10.0.0", + "inquirer": "^11.0.0", + "is-interactive": "^2.0.0", + "is-wsl": "^3.0.0", "js-beautify": "^1.11.0", - "karma": "^6.3.17", + "karma": "^6.4.4", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.3", - "karma-firefox-launcher": "^1.3.0", + "karma-firefox-launcher": "^2.0.0", "karma-mocha": "^2.0.1", "karma-mocha-reporter": "^2.2.5", "karma-sinon": "^1.0.5", "karma-sinon-chai": "^2.0.2", - "karma-sourcemap-loader": "^0.3.8", + "karma-sourcemap-loader": "^0.4.0", "karma-webpack": "^5.0.0", - "lodash": "^4.17.15", - "minimatch": "^3.0.4", + "lodash-es": "^4.17.21", + "minimatch": "^9.0.0", "minimist": "^1.2.8", - "mkdirp": "^1.0.4", - "mocha": "^7.1.2", + "mkdirp": "^3.0.0", + "mocha": "^10.0.0", "node-notifier": "^10.0.1", "process": "^0.11.10", "sinon": "^9.2.4", "sinon-chai": "^3.5.0", "socket.io": "^4.0.0", - "typescript": "^4.6.4", + "typescript": "5.0.4", "webpack": "^5.94.0" }, "devDependencies": { - "mockery": "^2.1.0", - "proxyquire": "^2.1.3" + "jest-extended": "^4.0.2", + "vitest": "^2.0.5" + }, + "scripts": { + "postinstall": "node bin/postinstall.js", + "test": "vitest run --config vitest.config.js", + "coverage": "vitest run --config vitest.config.js --coverage" }, "depcheckIgnore": [ "@babel/core", @@ -88,10 +94,5 @@ "mocha", "process", "typescript" - ], - "scripts": { - "postinstall": "node bin/postinstall.js", - "test": "mocha './tests/**/*.js' --timeout 10000", - "coverage": "nyc --reporter=lcov --reporter=text-summary yarn run test" - } + ] } diff --git a/packages/ckeditor5-dev-tests/tests/_utils/testsetup.js b/packages/ckeditor5-dev-tests/tests/_utils/testsetup.js new file mode 100644 index 000000000..52e540b96 --- /dev/null +++ b/packages/ckeditor5-dev-tests/tests/_utils/testsetup.js @@ -0,0 +1,9 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { expect } from 'vitest'; +import * as matchers from 'jest-extended'; + +expect.extend( matchers ); diff --git a/packages/ckeditor5-dev-tests/tests/fixtures/getdefinitionsfromfile/secret.cjs b/packages/ckeditor5-dev-tests/tests/fixtures/getdefinitionsfromfile/secret.cjs new file mode 100644 index 000000000..3f6701c37 --- /dev/null +++ b/packages/ckeditor5-dev-tests/tests/fixtures/getdefinitionsfromfile/secret.cjs @@ -0,0 +1,12 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +module.exports = { + SECRET: 'secret', + ANOTHER_SECRET: 'another-secret', + NON_PRIMITIVE_SECRET: { + foo: [ 'bar', 'baz' ] + } +}; diff --git a/packages/jsdoc-plugins/tests/comment-fixer.js b/packages/ckeditor5-dev-tests/tests/fixtures/karma-config-overrides/noop.cjs similarity index 82% rename from packages/jsdoc-plugins/tests/comment-fixer.js rename to packages/ckeditor5-dev-tests/tests/fixtures/karma-config-overrides/noop.cjs index 01c297b73..0b81095cf 100644 --- a/packages/jsdoc-plugins/tests/comment-fixer.js +++ b/packages/ckeditor5-dev-tests/tests/fixtures/karma-config-overrides/noop.cjs @@ -3,4 +3,5 @@ * For licensing, see LICENSE.md. */ -'use strict'; +module.exports = () => { +}; diff --git a/packages/ckeditor5-dev-tests/tests/fixtures/karma-config-overrides/removecoverage.cjs b/packages/ckeditor5-dev-tests/tests/fixtures/karma-config-overrides/removecoverage.cjs new file mode 100644 index 000000000..0828ebaed --- /dev/null +++ b/packages/ckeditor5-dev-tests/tests/fixtures/karma-config-overrides/removecoverage.cjs @@ -0,0 +1,8 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +module.exports = config => { + config.reporters.splice( config.reporters.indexOf( 'coverage' ), 1 ); +} diff --git a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/fixtures/file.js b/packages/ckeditor5-dev-tests/tests/fixtures/treatwarningsaserrorswebpackplugin/entrypoint.cjs similarity index 100% rename from packages/ckeditor5-dev-tests/tests/utils/automated-tests/fixtures/file.js rename to packages/ckeditor5-dev-tests/tests/fixtures/treatwarningsaserrorswebpackplugin/entrypoint.cjs diff --git a/packages/ckeditor5-dev-tests/tests/index.js b/packages/ckeditor5-dev-tests/tests/index.js new file mode 100644 index 000000000..15b9b0fc6 --- /dev/null +++ b/packages/ckeditor5-dev-tests/tests/index.js @@ -0,0 +1,37 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import * as index from '../lib/index.js'; +import runAutomatedTests from '../lib/tasks/runautomatedtests.js'; +import runManualTests from '../lib/tasks/runmanualtests.js'; +import parseArguments from '../lib/utils/automated-tests/parsearguments.js'; + +vi.mock( '../lib/tasks/runautomatedtests.js' ); +vi.mock( '../lib/tasks/runmanualtests.js' ); +vi.mock( '../lib/utils/automated-tests/parsearguments.js' ); + +describe( 'index.js', () => { + describe( 'runAutomatedTests()', () => { + it( 'should be a function', () => { + expect( index.runAutomatedTests ).to.be.a( 'function' ); + expect( index.runAutomatedTests ).toEqual( runAutomatedTests ); + } ); + } ); + + describe( 'runManualTests()', () => { + it( 'should be a function', () => { + expect( index.runManualTests ).to.be.a( 'function' ); + expect( index.runManualTests ).toEqual( runManualTests ); + } ); + } ); + + describe( 'parseArguments()', () => { + it( 'should be a function', () => { + expect( index.parseArguments ).to.be.a( 'function' ); + expect( index.parseArguments ).toEqual( parseArguments ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-tests/tests/tasks/runautomatedtests.js b/packages/ckeditor5-dev-tests/tests/tasks/runautomatedtests.js index 6ee2f09a9..86e39f4b8 100644 --- a/packages/ckeditor5-dev-tests/tests/tasks/runautomatedtests.js +++ b/packages/ckeditor5-dev-tests/tests/tasks/runautomatedtests.js @@ -3,95 +3,82 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const mockery = require( 'mockery' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); -const expect = require( 'chai' ).expect; -const chalk = require( 'chalk' ); -const path = require( 'path' ); - -describe( 'runAutomatedTests', () => { - let sandbox, stubs, runAutomatedTests, karmaServerCallback; - - beforeEach( () => { - karmaServerCallback = null; - sandbox = sinon.createSandbox(); - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - fs: { - writeFileSync: sandbox.stub(), - utimesSync: sandbox.stub(), - readdirSync: sandbox.stub() - }, - log: { - info: sandbox.stub(), - warn: sandbox.stub(), - error: sandbox.stub() - }, - mkdirp: { - sync: sandbox.stub() - }, - glob: { - globSync: sandbox.stub() - }, - karma: { - Server: class KarmaServer { - constructor( config, callback ) { - karmaServerCallback = callback; - } - - on( ...args ) { - return stubs.karma.karmaServerOn( ...args ); - } - - start( ...args ) { - return stubs.karma.karmaServerOn( ...args ); - } - }, - karmaServerOn: sandbox.stub(), - karmaServerStart: sandbox.stub(), - config: { - parseConfig: sandbox.stub() - } - }, - getKarmaConfig: sandbox.stub(), - transformFileOptionToTestGlob: sandbox.stub() - }; - - sandbox.stub( process, 'cwd' ).returns( '/workspace' ); +import path from 'path'; +import fs from 'fs'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { globSync } from 'glob'; +import { mkdirp } from 'mkdirp'; +import chalk from 'chalk'; +import karma from 'karma'; +import karmaLogger from 'karma/lib/logger.js'; +import transformFileOptionToTestGlob from '../../lib/utils/transformfileoptiontotestglob.js'; + +const stubs = vi.hoisted( () => ( { + log: { + log: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + info: vi.fn() + }, + karma: { + server: { + constructor: vi.fn(), + on: vi.fn(), + start: vi.fn() + } + } +} ) ); + +vi.mock( 'karma', () => ( { + default: { + Server: class KarmaServer { + constructor( ...args ) { + stubs.karma.server.constructor( ...args ); + } - mockery.registerMock( 'mkdirp', stubs.mkdirp ); - mockery.registerMock( 'karma', stubs.karma ); - mockery.registerMock( 'karma/lib/logger.js', { - setupFromConfig: sandbox.spy(), - create( name ) { - expect( name ).to.equal( 'config' ); - return stubs.log; + on( ...args ) { + return stubs.karma.server.on( ...args ); } - } ); - mockery.registerMock( '../utils/automated-tests/getkarmaconfig', stubs.getKarmaConfig ); - mockery.registerMock( '../utils/transformfileoptiontotestglob', stubs.transformFileOptionToTestGlob ); - runAutomatedTests = proxyquire( '../../lib/tasks/runautomatedtests', { - fs: stubs.fs, - glob: stubs.glob + start( ...args ) { + return stubs.karma.server.start( ...args ); + } + }, + config: { + parseConfig: vi.fn() + } + } +} ) ); + +vi.mock( 'chalk', () => ( { + default: { + yellow: vi.fn() + } +} ) ); + +vi.mock( 'fs' ); +vi.mock( 'mkdirp' ); +vi.mock( 'glob' ); +vi.mock( 'karma/lib/logger.js' ); +vi.mock( '../../lib/utils/automated-tests/getkarmaconfig.js' ); +vi.mock( '../../lib/utils/transformfileoptiontotestglob.js' ); + +describe( 'runAutomatedTests()', () => { + let runAutomatedTests; + + beforeEach( async () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/workspace' ); + + vi.mocked( karmaLogger ).create.mockImplementation( name => { + expect( name ).to.equal( 'config' ); + + return stubs.log; } ); - } ); - afterEach( () => { - sandbox.restore(); - mockery.disable(); + runAutomatedTests = ( await import( '../../lib/tasks/runautomatedtests.js' ) ).default; } ); - it( 'should create an entry file before tests execution', done => { + it( 'should create an entry file before tests execution', async () => { const options = { files: [ 'basic-styles' @@ -99,56 +86,51 @@ describe( 'runAutomatedTests', () => { production: true }; - stubs.fs.readdirSync.returns( [] ); + vi.mocked( fs ).readdirSync.mockReturnValue( [] ); - stubs.transformFileOptionToTestGlob.returns( [ + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ '/workspace/packages/ckeditor5-basic-styles/tests/**/*.js', '/workspace/packages/ckeditor-basic-styles/tests/**/*.js' ] ); - stubs.glob.globSync.onFirstCall().returns( [ - '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', - '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' - ] ); - - stubs.glob.globSync.onSecondCall().returns( [] ); + vi.mocked( globSync ) + .mockReturnValue( [] ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', + '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' + ] ); const expectedEntryPointContent = [ 'import "/workspace/packages/ckeditor5-basic-styles/tests/bold.js";', - 'import "/workspace/packages/ckeditor5-basic-styles/tests/italic.js";', - '' + 'import "/workspace/packages/ckeditor5-basic-styles/tests/italic.js";' ].join( '\n' ); + const promise = runAutomatedTests( options ); + setTimeout( () => { - karmaServerCallback( 0 ); - } ); + expect( stubs.karma.server.constructor ).toHaveBeenCalledOnce(); + + const [ firstCall ] = stubs.karma.server.constructor.mock.calls; + const [ , exitCallback ] = firstCall; - runAutomatedTests( options ) - .then( () => { - expect( stubs.mkdirp.sync.calledOnce ).to.equal( true ); - expect( stubs.mkdirp.sync.firstCall.args[ 0 ] ).to.equal( '/workspace/build/.automated-tests' ); + exitCallback( 0 ); + } ); - expect( stubs.fs.writeFileSync.calledOnce ).to.equal( true ); - expect( stubs.fs.writeFileSync.firstCall.args[ 0 ] ).to.equal( '/workspace/build/.automated-tests/entry-point.js' ); - expect( stubs.fs.writeFileSync.firstCall.args[ 1 ] ).to.include( expectedEntryPointContent ); + await promise; - done(); - } ); + expect( vi.mocked( mkdirp ).sync ).toHaveBeenCalledExactlyOnceWith( '/workspace/build/.automated-tests' ); + expect( vi.mocked( fs ).writeFileSync ).toHaveBeenCalledExactlyOnceWith( + '/workspace/build/.automated-tests/entry-point.js', + expect.stringContaining( expectedEntryPointContent ) + ); } ); - it( 'throws when files are not specified', () => { - return runAutomatedTests( { production: true } ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - err => { - expect( err.message ).to.equal( 'Karma requires files to tests. `options.files` has to be non-empty array.' ); - } - ); + it( 'throws when files are not specified', async () => { + await expect( runAutomatedTests( { production: true } ) ) + .rejects.toThrow( 'Karma requires files to tests. `options.files` has to be non-empty array.' ); } ); - it( 'throws when specified files are invalid', () => { + it( 'throws when specified files are invalid', async () => { const options = { files: [ 'basic-foo', @@ -157,38 +139,29 @@ describe( 'runAutomatedTests', () => { production: true }; - stubs.fs.readdirSync.returns( [] ); + vi.mocked( fs ).readdirSync.mockReturnValue( [] ); - stubs.transformFileOptionToTestGlob.onFirstCall().returns( [ - '/workspace/packages/ckeditor5-basic-foo/tests/**/*.js', - '/workspace/packages/ckeditor-basic-foo/tests/**/*.js' - ] ); + vi.mocked( transformFileOptionToTestGlob ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-basic-foo/tests/**/*.js', + '/workspace/packages/ckeditor-basic-foo/tests/**/*.js' + ] ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-bar-core/tests/**/*.js', + '/workspace/packages/ckeditor-bar-core/tests/**/*.js' + ] ); - stubs.transformFileOptionToTestGlob.onSecondCall().returns( [ - '/workspace/packages/ckeditor5-bar-core/tests/**/*.js', - '/workspace/packages/ckeditor-bar-core/tests/**/*.js' - ] ); + vi.mocked( globSync ).mockReturnValue( [] ); + + await expect( runAutomatedTests( options ) ) + .rejects.toThrow( 'Not found files to tests. Specified patterns are invalid.' ); - stubs.glob.globSync.returns( [] ); - - return runAutomatedTests( options ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - err => { - expect( stubs.log.warn.calledTwice ).to.equal( true ); - expect( stubs.log.warn.firstCall.args[ 0 ] ).to.equal( 'Pattern "%s" does not match any file.' ); - expect( stubs.log.warn.firstCall.args[ 1 ] ).to.equal( 'basic-foo' ); - expect( stubs.log.warn.secondCall.args[ 0 ] ).to.equal( 'Pattern "%s" does not match any file.' ); - expect( stubs.log.warn.secondCall.args[ 1 ] ).to.equal( 'bar-core' ); - - expect( err.message ).to.equal( 'Not found files to tests. Specified patterns are invalid.' ); - } - ); + expect( stubs.log.warn ).toHaveBeenCalledTimes( 2 ); + expect( stubs.log.warn ).toHaveBeenCalledWith( 'Pattern "%s" does not match any file.', 'basic-foo' ); + expect( stubs.log.warn ).toHaveBeenCalledWith( 'Pattern "%s" does not match any file.', 'bar-core' ); } ); - it( 'throws when Karma config parser throws', () => { + it( 'throws when Karma config parser throws', async () => { const options = { files: [ 'basic-styles' @@ -196,34 +169,34 @@ describe( 'runAutomatedTests', () => { production: true }; - stubs.fs.readdirSync.returns( [] ); - - stubs.transformFileOptionToTestGlob.returns( [ - '/workspace/packages/ckeditor5-basic-styles/tests/**/*.js', - '/workspace/packages/ckeditor-basic-styles/tests/**/*.js' - ] ); - - stubs.glob.globSync.onFirstCall().returns( [ - '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', - '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' - ] ); - - stubs.glob.globSync.onSecondCall().returns( [] ); - - stubs.karma.config.parseConfig.throws( new Error( 'Example error from Karma config parser.' ) ); + vi.mocked( fs ).readdirSync.mockReturnValue( [] ); + + vi.mocked( transformFileOptionToTestGlob ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-basic-foo/tests/**/*.js', + '/workspace/packages/ckeditor-basic-foo/tests/**/*.js' + ] ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-bar-core/tests/**/*.js', + '/workspace/packages/ckeditor-bar-core/tests/**/*.js' + ] ); + + vi.mocked( globSync ) + .mockReturnValue( [] ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', + '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' + ] ); + + vi.mocked( karma ).config.parseConfig.mockImplementation( () => { + throw new Error( 'Example error from Karma config parser.' ); + } ); - return runAutomatedTests( options ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - err => { - expect( err.message ).to.equal( 'Example error from Karma config parser.' ); - } - ); + await expect( runAutomatedTests( options ) ) + .rejects.toThrow( 'Example error from Karma config parser.' ); } ); - it( 'should warn when the `production` option is set to `false`', () => { + it( 'should warn when the `production` option is set to `false`', async () => { const options = { files: [ 'basic-styles' @@ -231,37 +204,44 @@ describe( 'runAutomatedTests', () => { production: false }; - const consoleWarnStub = sandbox.stub( console, 'warn' ); + const consoleWarnStub = vi.spyOn( console, 'warn' ).mockImplementation( () => {} ); - stubs.fs.readdirSync.returns( [] ); + vi.mocked( chalk ).yellow.mockReturnValue( 'chalk.yellow: warn' ); + vi.mocked( fs ).readdirSync.mockReturnValue( [] ); - stubs.transformFileOptionToTestGlob.returns( [ + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ '/workspace/packages/ckeditor5-basic-styles/tests/**/*.js', '/workspace/packages/ckeditor-basic-styles/tests/**/*.js' ] ); - stubs.glob.globSync.onFirstCall().returns( [ - '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', - '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' - ] ); + vi.mocked( globSync ) + .mockReturnValue( [] ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', + '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' + ] ); - stubs.glob.globSync.onSecondCall().returns( [] ); + const promise = runAutomatedTests( options ); setTimeout( () => { - karmaServerCallback( 0 ); + expect( stubs.karma.server.constructor ).toHaveBeenCalledOnce(); + + const [ firstCall ] = stubs.karma.server.constructor.mock.calls; + const [ , exitCallback ] = firstCall; + + exitCallback( 0 ); } ); - return runAutomatedTests( options ) - .then( () => { - expect( consoleWarnStub ).to.be.calledOnce; - expect( consoleWarnStub ).to.be.calledWith( - chalk.yellow( '⚠ You\'re running tests in dev mode - some error protections are loose. ' + - 'Use the `--production` flag to use strictest verification methods.' ) - ); - } ); + await promise; + + expect( consoleWarnStub ).toHaveBeenCalledExactlyOnceWith( 'chalk.yellow: warn' ); + expect( vi.mocked( chalk ).yellow ).toHaveBeenCalledExactlyOnceWith( + '⚠ You\'re running tests in dev mode - some error protections are loose. ' + + 'Use the `--production` flag to use strictest verification methods.' + ); } ); - it( 'should not add the code making console use throw an error when the `production` option is set to false', () => { + it( 'should not add the code making console use throw an error when the `production` option is set to false', async () => { const options = { files: [ 'basic-styles' @@ -269,33 +249,42 @@ describe( 'runAutomatedTests', () => { production: false }; - sandbox.stub( console, 'warn' ); - - stubs.fs.readdirSync.returns( [] ); + vi.spyOn( console, 'warn' ).mockImplementation( () => { + } ); + vi.mocked( fs ).readdirSync.mockReturnValue( [] ); - stubs.transformFileOptionToTestGlob.returns( [ + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ '/workspace/packages/ckeditor5-basic-styles/tests/**/*.js', '/workspace/packages/ckeditor-basic-styles/tests/**/*.js' ] ); - stubs.glob.globSync.onFirstCall().returns( [ - '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', - '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' - ] ); + vi.mocked( globSync ) + .mockReturnValue( [] ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', + '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' + ] ); - stubs.glob.globSync.onSecondCall().returns( [] ); + const promise = runAutomatedTests( options ); setTimeout( () => { - karmaServerCallback( 0 ); + expect( stubs.karma.server.constructor ).toHaveBeenCalledOnce(); + + const [ firstCall ] = stubs.karma.server.constructor.mock.calls; + const [ , exitCallback ] = firstCall; + + exitCallback( 0 ); } ); - return runAutomatedTests( options ) - .then( () => { - expect( stubs.fs.writeFileSync.firstCall.args[ 1 ] ).to.not.include( '// Make using any method from the console to fail.' ); - } ); + await promise; + + expect( vi.mocked( fs ).writeFileSync ).toHaveBeenCalledExactlyOnceWith( + expect.any( String ), + expect.not.stringContaining( '// Make using any method from the console to fail.' ) + ); } ); - it( 'should add the code making console use throw an error when the `production` option is set to true', () => { + it( 'should add the code making console use throw an error when the `production` option is set to true', async () => { const options = { files: [ 'basic-styles' @@ -303,31 +292,42 @@ describe( 'runAutomatedTests', () => { production: true }; - stubs.fs.readdirSync.returns( [] ); + vi.spyOn( console, 'warn' ).mockImplementation( () => { + } ); + vi.mocked( fs ).readdirSync.mockReturnValue( [] ); - stubs.transformFileOptionToTestGlob.returns( [ + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ '/workspace/packages/ckeditor5-basic-styles/tests/**/*.js', '/workspace/packages/ckeditor-basic-styles/tests/**/*.js' ] ); - stubs.glob.globSync.onFirstCall().returns( [ - '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', - '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' - ] ); + vi.mocked( globSync ) + .mockReturnValue( [] ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', + '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' + ] ); - stubs.glob.globSync.onSecondCall().returns( [] ); + const promise = runAutomatedTests( options ); setTimeout( () => { - karmaServerCallback( 0 ); + expect( stubs.karma.server.constructor ).toHaveBeenCalledOnce(); + + const [ firstCall ] = stubs.karma.server.constructor.mock.calls; + const [ , exitCallback ] = firstCall; + + exitCallback( 0 ); } ); - return runAutomatedTests( options ) - .then( () => { - expect( stubs.fs.writeFileSync.firstCall.args[ 1 ] ).to.include( '// Make using any method from the console to fail.' ); - } ); + await promise; + + expect( vi.mocked( fs ).writeFileSync ).toHaveBeenCalledExactlyOnceWith( + expect.any( String ), + expect.stringContaining( '// Make using any method from the console to fail.' ) + ); } ); - it( 'should load custom assertions automatically (camelCase)', done => { + it( 'should load custom assertions automatically (camelCase in paths)', async () => { const options = { files: [ 'basic-styles' @@ -335,19 +335,19 @@ describe( 'runAutomatedTests', () => { production: true }; - stubs.fs.readdirSync.returns( [ 'assertionA.js', 'assertionB.js' ] ); + vi.mocked( fs ).readdirSync.mockReturnValue( [ 'assertionA.js', 'assertionB.js' ] ); - stubs.transformFileOptionToTestGlob.returns( [ + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ '/workspace/packages/ckeditor5-basic-styles/tests/**/*.js', '/workspace/packages/ckeditor-basic-styles/tests/**/*.js' ] ); - stubs.glob.globSync.onFirstCall().returns( [ - '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', - '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' - ] ); - - stubs.glob.globSync.onSecondCall().returns( [] ); + vi.mocked( globSync ) + .mockReturnValue( [] ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', + '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' + ] ); const assertionsDir = path.join( __dirname, '..', '..', 'lib', 'utils', 'automated-tests', 'assertions' ).replace( /\\/g, '/' ); @@ -355,28 +355,30 @@ describe( 'runAutomatedTests', () => { `import assertionAFactory from "${ assertionsDir }/assertionA.js";`, `import assertionBFactory from "${ assertionsDir }/assertionB.js";`, 'assertionAFactory( chai );', - 'assertionBFactory( chai );', - '' + 'assertionBFactory( chai );' ].join( '\n' ); + const promise = runAutomatedTests( options ); + setTimeout( () => { - karmaServerCallback( 0 ); - } ); + expect( stubs.karma.server.constructor ).toHaveBeenCalledOnce(); + + const [ firstCall ] = stubs.karma.server.constructor.mock.calls; + const [ , exitCallback ] = firstCall; - runAutomatedTests( options ) - .then( () => { - expect( stubs.mkdirp.sync.calledOnce ).to.equal( true ); - expect( stubs.mkdirp.sync.firstCall.args[ 0 ] ).to.equal( '/workspace/build/.automated-tests' ); + exitCallback( 0 ); + } ); - expect( stubs.fs.writeFileSync.calledOnce ).to.equal( true ); - expect( stubs.fs.writeFileSync.firstCall.args[ 0 ] ).to.equal( '/workspace/build/.automated-tests/entry-point.js' ); - expect( stubs.fs.writeFileSync.firstCall.args[ 1 ] ).to.include( expectedEntryPointContent ); + await promise; - done(); - } ); + expect( vi.mocked( mkdirp ).sync ).toHaveBeenCalledExactlyOnceWith( '/workspace/build/.automated-tests' ); + expect( vi.mocked( fs ).writeFileSync ).toHaveBeenCalledExactlyOnceWith( + '/workspace/build/.automated-tests/entry-point.js', + expect.stringContaining( expectedEntryPointContent ) + ); } ); - it( 'should load custom assertions automatically (kebab-case)', done => { + it( 'should load custom assertions automatically (kebab-case in paths)', async () => { const options = { files: [ 'basic-styles' @@ -384,19 +386,19 @@ describe( 'runAutomatedTests', () => { production: true }; - stubs.fs.readdirSync.returns( [ 'assertion-a.js', 'assertion-b.js' ] ); + vi.mocked( fs ).readdirSync.mockReturnValue( [ 'assertion-a.js', 'assertion-b.js' ] ); - stubs.transformFileOptionToTestGlob.returns( [ + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ '/workspace/packages/ckeditor5-basic-styles/tests/**/*.js', '/workspace/packages/ckeditor-basic-styles/tests/**/*.js' ] ); - stubs.glob.globSync.onFirstCall().returns( [ - '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', - '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' - ] ); - - stubs.glob.globSync.onSecondCall().returns( [] ); + vi.mocked( globSync ) + .mockReturnValue( [] ) + .mockReturnValueOnce( [ + '/workspace/packages/ckeditor5-basic-styles/tests/bold.js', + '/workspace/packages/ckeditor5-basic-styles/tests/italic.js' + ] ); const assertionsDir = path.join( __dirname, '..', '..', 'lib', 'utils', 'automated-tests', 'assertions' ).replace( /\\/g, '/' ); @@ -408,20 +410,23 @@ describe( 'runAutomatedTests', () => { '' ].join( '\n' ); + const promise = runAutomatedTests( options ); + setTimeout( () => { - karmaServerCallback( 0 ); - } ); + expect( stubs.karma.server.constructor ).toHaveBeenCalledOnce(); + + const [ firstCall ] = stubs.karma.server.constructor.mock.calls; + const [ , exitCallback ] = firstCall; - runAutomatedTests( options ) - .then( () => { - expect( stubs.mkdirp.sync.calledOnce ).to.equal( true ); - expect( stubs.mkdirp.sync.firstCall.args[ 0 ] ).to.equal( '/workspace/build/.automated-tests' ); + exitCallback( 0 ); + } ); - expect( stubs.fs.writeFileSync.calledOnce ).to.equal( true ); - expect( stubs.fs.writeFileSync.firstCall.args[ 0 ] ).to.equal( '/workspace/build/.automated-tests/entry-point.js' ); - expect( stubs.fs.writeFileSync.firstCall.args[ 1 ] ).to.include( expectedEntryPointContent ); + await promise; - done(); - } ); + expect( vi.mocked( mkdirp ).sync ).toHaveBeenCalledExactlyOnceWith( '/workspace/build/.automated-tests' ); + expect( vi.mocked( fs ).writeFileSync ).toHaveBeenCalledExactlyOnceWith( + '/workspace/build/.automated-tests/entry-point.js', + expect.stringContaining( expectedEntryPointContent ) + ); } ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/tasks/runmanualtests.js b/packages/ckeditor5-dev-tests/tests/tasks/runmanualtests.js index f59ca777d..ab8791403 100644 --- a/packages/ckeditor5-dev-tests/tests/tasks/runmanualtests.js +++ b/packages/ckeditor5-dev-tests/tests/tasks/runmanualtests.js @@ -3,235 +3,211 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import fs from 'fs'; +import path from 'path'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Server } from 'socket.io'; +import { spawn } from 'child_process'; +import { globSync } from 'glob'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import isInteractive from 'is-interactive'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import requireDll from '../../lib/utils/requiredll.js'; +import createManualTestServer from '../../lib/utils/manual-tests/createserver.js'; +import compileManualTestScripts from '../../lib/utils/manual-tests/compilescripts.js'; +import compileManualTestHtmlFiles from '../../lib/utils/manual-tests/compilehtmlfiles.js'; +import transformFileOptionToTestGlob from '../../lib/utils/transformfileoptiontotestglob.js'; +import removeDir from '../../lib/utils/manual-tests/removedir.js'; +import runManualTests from '../../lib/tasks/runmanualtests.js'; + +const stubs = vi.hoisted( () => ( { + log: { + log: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn() + }, + spawn: { + spawnReturnValue: { + on: ( event, callback ) => { + if ( !stubs.spawn.spawnEvents[ event ] ) { + stubs.spawn.spawnEvents[ event ] = []; + } -const mockery = require( 'mockery' ); -const sinon = require( 'sinon' ); -const expect = require( 'chai' ).expect; + stubs.spawn.spawnEvents[ event ].push( callback ); + + // Return the same object containing the `on()` method to allow method chaining: `.on( ... ).on( ... )`. + return stubs.spawn.spawnReturnValue; + } + }, + spawnExitCode: 0, + spawnEvents: {}, + spawnTriggerEvent: ( event, data ) => { + if ( stubs.spawn.spawnEvents[ event ] ) { + for ( const callback of stubs.spawn.spawnEvents[ event ] ) { + callback( data ); + } -describe( 'runManualTests', () => { - let sandbox, spies, runManualTests, defaultOptions; + delete stubs.spawn.spawnEvents[ event ]; + } + } + } +} ) ); + +vi.mock( 'socket.io' ); +vi.mock( 'child_process' ); +vi.mock( 'inquirer' ); +vi.mock( 'glob' ); +vi.mock( 'chalk', () => ( { + default: { + bold: vi.fn( input => input ) + } +} ) ); +vi.mock( 'path' ); +vi.mock( 'fs' ); +vi.mock( 'is-interactive' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( '../../lib/utils/manual-tests/createserver.js' ); +vi.mock( '../../lib/utils/manual-tests/compilehtmlfiles.js' ); +vi.mock( '../../lib/utils/manual-tests/compilescripts.js' ); +vi.mock( '../../lib/utils/manual-tests/removedir.js' ); +vi.mock( '../../lib/utils/manual-tests/copyassets.js' ); +vi.mock( '../../lib/utils/transformfileoptiontotestglob.js' ); +vi.mock( '../../lib/utils/requiredll.js' ); + +describe( 'runManualTests()', () => { + let defaultOptions; beforeEach( () => { - sandbox = sinon.createSandbox(); + stubs.spawn.spawnExitCode = 0; + + vi.mocked( spawn ).mockImplementation( () => { + // Simulate closing a new process. It does not matter that this simulation ends the child process immediately. + // All that matters is that the `close` event is emitted with specified exit code. + process.nextTick( () => { + stubs.spawn.spawnTriggerEvent( 'close', stubs.spawn.spawnExitCode ); + } ); - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false + return stubs.spawn.spawnReturnValue; } ); - spies = { - socketIO: { - Server: sandbox.stub().returns( new ( class {} )() ) - }, - childProcess: { - spawn: sandbox.stub().callsFake( () => { - // Simulate closing a new process. It does not matter that this simulation ends the child process immediately. - // All that matters is that the `close` event is emitted with specified exit code. - process.nextTick( () => { - spies.childProcess.spawnTriggerEvent( 'close', spies.childProcess.spawnExitCode ); - } ); + vi.mocked( globSync ).mockImplementation( pattern => { + const patterns = { + // Valid pattern for manual tests. + 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js': [ + 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', + 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js' + ], + // Another valid pattern for manual tests. + 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js': [ + 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', + 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' + ], + // Invalid pattern for manual tests (points to `/_utils/` subdirectory). + 'workspace/packages/ckeditor-*/tests/**/manual/_utils/**/*.js': [ + 'workspace/packages/ckeditor-foo/tests/manual/_utils/feature-e.js', + 'workspace/packages/ckeditor-bar/tests/manual/_utils/feature-f.js' + ], + // Invalid pattern for manual tests (points outside manual test directory). + 'workspace/packages/ckeditor-*/tests/**/outside/**/*.js': [ + 'workspace/packages/ckeditor-foo/tests/outside/feature-g.js', + 'workspace/packages/ckeditor-bar/tests/outside/feature-h.js' + ], + // Valid pattern for manual tests that require DLLs. + 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js': [ + 'workspace/packages/ckeditor5-foo/tests/manual/dll/feature-i-dll.js', + 'workspace/packages/ckeditor5-bar/tests/manual/dll/feature-j-dll.js' + ], + // Pattern for finding `package.json` in all repositories. + // External repositories are first, then the root repository. + '{,external/*/}package.json': [ + 'workspace/ckeditor5/external/ckeditor5-internal/package.json', + 'workspace/ckeditor5/external/collaboration-features/package.json', + 'workspace/ckeditor5/package.json' + ] + }; - return spies.childProcess.spawnReturnValue; - } ), - spawnReturnValue: { - on: ( event, callback ) => { - if ( !spies.childProcess.spawnEvents[ event ] ) { - spies.childProcess.spawnEvents[ event ] = []; - } + const separator = process.platform === 'win32' ? '\\' : '/'; + const result = patterns[ pattern ] || []; - spies.childProcess.spawnEvents[ event ].push( callback ); + return result.map( p => p.split( '/' ).join( separator ) ); + } ); - // Return the same object containing the `on()` method to allow method chaining: `.on( ... ).on( ... )`. - return spies.childProcess.spawnReturnValue; - } - }, - spawnExitCode: 0, - spawnEvents: {}, - spawnTriggerEvent: ( event, data ) => { - if ( spies.childProcess.spawnEvents[ event ] ) { - for ( const callback of spies.childProcess.spawnEvents[ event ] ) { - callback( data ); - } + vi.mocked( path ).join.mockImplementation( ( ...chunks ) => chunks.join( '/' ) ); + vi.mocked( path ).resolve.mockImplementation( path => '/absolute/path/to/' + path ); + vi.mocked( path ).basename.mockImplementation( path => path.split( /[\\/]/ ).pop() ); + vi.mocked( path ).dirname.mockImplementation( path => { + const chunks = path.split( /[\\/]/ ); - delete spies.childProcess.spawnEvents[ event ]; - } - } - }, - inquirer: { - prompt: sandbox.stub() - }, - glob: { - globSync: sandbox.stub().callsFake( pattern => { - const patterns = { - // Valid pattern for manual tests. - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js': [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js' - ], - // Another valid pattern for manual tests. - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js': [ - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - // Invalid pattern for manual tests (points to `/_utils/` subdirectory). - 'workspace/packages/ckeditor-*/tests/**/manual/_utils/**/*.js': [ - 'workspace/packages/ckeditor-foo/tests/manual/_utils/feature-e.js', - 'workspace/packages/ckeditor-bar/tests/manual/_utils/feature-f.js' - ], - // Invalid pattern for manual tests (points outside manual test directory). - 'workspace/packages/ckeditor-*/tests/**/outside/**/*.js': [ - 'workspace/packages/ckeditor-foo/tests/outside/feature-g.js', - 'workspace/packages/ckeditor-bar/tests/outside/feature-h.js' - ], - // Valid pattern for manual tests that require DLLs. - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js': [ - 'workspace/packages/ckeditor5-foo/tests/manual/dll/feature-i-dll.js', - 'workspace/packages/ckeditor5-bar/tests/manual/dll/feature-j-dll.js' - ], - // Pattern for finding `package.json` in all repositories. - // External repositories are first, then the root repository. - '{,external/*/}package.json': [ - 'workspace/ckeditor5/external/ckeditor5-internal/package.json', - 'workspace/ckeditor5/external/collaboration-features/package.json', - 'workspace/ckeditor5/package.json' - ] - }; - - const separator = process.platform === 'win32' ? '\\' : '/'; - const result = patterns[ pattern ] || []; - - return result.map( p => p.split( '/' ).join( separator ) ); - } ) - }, - chalk: { - bold: sandbox.stub().callsFake( msg => msg ) - }, - fs: { - readFileSync: sandbox.stub() - }, - path: { - join: sandbox.stub().callsFake( ( ...chunks ) => chunks.join( '/' ) ), - resolve: sandbox.stub().callsFake( path => '/absolute/path/to/' + path ), - basename: sandbox.stub().callsFake( path => path.split( /[\\/]/ ).pop() ), - dirname: sandbox.stub().callsFake( path => { - const chunks = path.split( /[\\/]/ ); - - chunks.pop(); - - return chunks.join( '/' ); - } ) - }, - devUtils: { - logger: sandbox.stub().callsFake( () => ( { - warning: spies.devUtils.logWarning, - info: spies.devUtils.logInfo - } ) ), - logWarning: sandbox.stub(), - logInfo: sandbox.stub() - }, - isInteractive: sandbox.stub(), - requireDll: sandbox.stub().callsFake( sourceFiles => { - return sourceFiles.some( filePath => /-dll.[jt]s$/.test( filePath ) ); - } ), - server: sandbox.stub(), - htmlFileCompiler: sandbox.spy( () => Promise.resolve() ), - scriptCompiler: sandbox.spy( () => Promise.resolve() ), - removeDir: sandbox.spy( () => Promise.resolve() ), - copyAssets: sandbox.spy(), - transformFileOptionToTestGlob: sandbox.stub().returns( [] ) - }; + chunks.pop(); + + return chunks.join( '/' ); + } ); - mockery.registerMock( 'socket.io', spies.socketIO ); - mockery.registerMock( 'child_process', spies.childProcess ); - mockery.registerMock( 'inquirer', spies.inquirer ); - mockery.registerMock( 'glob', spies.glob ); - mockery.registerMock( 'chalk', spies.chalk ); - mockery.registerMock( 'path', spies.path ); - mockery.registerMock( 'fs', spies.fs ); - mockery.registerMock( 'is-interactive', spies.isInteractive ); - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', spies.devUtils ); - mockery.registerMock( '../utils/manual-tests/createserver', spies.server ); - mockery.registerMock( '../utils/manual-tests/compilehtmlfiles', spies.htmlFileCompiler ); - mockery.registerMock( '../utils/manual-tests/compilescripts', spies.scriptCompiler ); - mockery.registerMock( '../utils/manual-tests/removedir', spies.removeDir ); - mockery.registerMock( '../utils/manual-tests/copyassets', spies.copyAssets ); - mockery.registerMock( '../utils/transformfileoptiontotestglob', spies.transformFileOptionToTestGlob ); - mockery.registerMock( '../utils/requiredll', spies.requireDll ); - - sandbox.stub( process, 'cwd' ).returns( 'workspace' ); + vi.mocked( logger ).mockImplementation( () => stubs.log ); + vi.mocked( requireDll ).mockImplementation( sourceFiles => { + return sourceFiles.some( filePath => /-dll.[jt]s$/.test( filePath ) ); + } ); + + vi.mocked( compileManualTestScripts ).mockResolvedValue(); + vi.mocked( compileManualTestHtmlFiles ).mockResolvedValue(); + vi.mocked( removeDir ).mockResolvedValue(); + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [] ); + + vi.spyOn( process, 'cwd' ).mockReturnValue( 'workspace' ); // The `glob` util returns paths in format depending on the platform. - sandbox.stub( process, 'platform' ).value( 'linux' ); + vi.spyOn( process, 'platform', 'get' ).mockReturnValue( 'linux' ); defaultOptions = { dll: null }; - - runManualTests = require( '../../lib/tasks/runmanualtests' ); } ); - afterEach( () => { - sandbox.restore(); - mockery.disable(); - } ); - - it( 'should run all manual tests and return promise', () => { - spies.transformFileOptionToTestGlob.returns( [ + it( 'should run all manual tests and return promise', async () => { + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); - return runManualTests( defaultOptions ) - .then( () => { - expect( spies.removeDir.calledOnce ).to.equal( true ); - expect( spies.removeDir.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - - expect( spies.htmlFileCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.htmlFileCompiler.firstCall, { - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - language: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - disableWatch: true, - silent: false - } ); - - expect( spies.scriptCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - themePath: null, - language: undefined, - tsconfig: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - debug: undefined, - disableWatch: true, - identityFile: undefined - } ); - - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - } ); + await runManualTests( defaultOptions ); + + expect( vi.mocked( removeDir ) ).toHaveBeenCalledExactlyOnceWith( 'workspace/build/.manual-tests', expect.any( Object ) ); + expect( vi.mocked( createManualTestServer ) ).toHaveBeenCalledExactlyOnceWith( + 'workspace/build/.manual-tests', + undefined, + expect.any( Function ) + ); + expect( vi.mocked( compileManualTestHtmlFiles ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + buildDir: 'workspace/build/.manual-tests', + sourceFiles: [ + 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', + 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', + 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', + 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' + ], + onTestCompilationStatus: expect.any( Function ), + silent: false + } ) ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + cwd: 'workspace', + buildDir: 'workspace/build/.manual-tests', + sourceFiles: [ + 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', + 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', + 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', + 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' + ], + themePath: null, + onTestCompilationStatus: expect.any( Function ) + } ) ); } ); - it( 'runs specified manual tests', () => { - spies.transformFileOptionToTestGlob.onFirstCall().returns( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js' ] ); - spies.transformFileOptionToTestGlob.onSecondCall().returns( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); + it( 'runs specified manual tests', async () => { + vi.mocked( transformFileOptionToTestGlob ) + .mockReturnValueOnce( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js' ] ) + .mockReturnValueOnce( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); const options = { files: [ @@ -242,62 +218,31 @@ describe( 'runManualTests', () => { debug: [ 'CK_DEBUG' ] }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.removeDir.calledOnce ).to.equal( true ); - expect( spies.removeDir.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - - expect( spies.transformFileOptionToTestGlob.calledTwice ).to.equal( true ); - expect( spies.transformFileOptionToTestGlob.firstCall.args[ 0 ] ).to.equal( 'ckeditor5-classic' ); - expect( spies.transformFileOptionToTestGlob.firstCall.args[ 1 ] ).to.equal( true ); - expect( spies.transformFileOptionToTestGlob.secondCall.args[ 0 ] ).to.equal( 'ckeditor-classic/manual/classic.js' ); - expect( spies.transformFileOptionToTestGlob.secondCall.args[ 1 ] ).to.equal( true ); - - expect( spies.htmlFileCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.htmlFileCompiler.firstCall, { - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - language: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - disableWatch: false, - silent: false - } ); - - expect( spies.scriptCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - themePath: 'path/to/theme', - language: undefined, - tsconfig: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - debug: [ 'CK_DEBUG' ], - disableWatch: false, - identityFile: undefined - } ); - - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - } ); + await runManualTests( { ...defaultOptions, ...options } ); + + expect( vi.mocked( transformFileOptionToTestGlob ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( transformFileOptionToTestGlob ) ).toHaveBeenCalledWith( 'ckeditor5-classic', true ); + expect( vi.mocked( transformFileOptionToTestGlob ) ).toHaveBeenCalledWith( 'ckeditor-classic/manual/classic.js', true ); + expect( vi.mocked( compileManualTestHtmlFiles ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + sourceFiles: [ + 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', + 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', + 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', + 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' + ] + } ) ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + sourceFiles: [ + 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', + 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', + 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', + 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' + ], + debug: [ 'CK_DEBUG' ] + } ) ); } ); - it( 'allows specifying language and additionalLanguages (to CKEditorTranslationsPlugin)', () => { - spies.transformFileOptionToTestGlob.onFirstCall().returns( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js' ] ); - spies.transformFileOptionToTestGlob.onSecondCall().returns( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); - + it( 'allows specifying language and additionalLanguages (to `CKEditorTranslationsPlugin`)', async () => { const options = { files: [ 'ckeditor5-classic', @@ -312,55 +257,17 @@ describe( 'runManualTests', () => { debug: [ 'CK_DEBUG' ] }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.removeDir.calledOnce ).to.equal( true ); - expect( spies.removeDir.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - - expect( spies.htmlFileCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.htmlFileCompiler.firstCall, { - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - language: 'pl', - onTestCompilationStatus: sinon.match.func, - additionalLanguages: [ 'ar', 'en' ], - disableWatch: false, - silent: false - } ); - - expect( spies.scriptCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - themePath: 'path/to/theme', - language: 'pl', - onTestCompilationStatus: sinon.match.func, - additionalLanguages: [ 'ar', 'en' ], - debug: [ 'CK_DEBUG' ], - disableWatch: false, - tsconfig: undefined, - identityFile: undefined - } ); + await runManualTests( { ...defaultOptions, ...options } ); - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - } ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + additionalLanguages: [ 'ar', 'en' ] + } ) ); } ); - it( 'allows specifying port', () => { - spies.transformFileOptionToTestGlob.onFirstCall().returns( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js' ] ); - spies.transformFileOptionToTestGlob.onSecondCall().returns( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); + it( 'allows specifying port', async () => { + vi.mocked( transformFileOptionToTestGlob ) + .mockReturnValueOnce( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js' ] ) + .mockReturnValueOnce( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); const options = { files: [ @@ -370,18 +277,16 @@ describe( 'runManualTests', () => { port: 8888 }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - expect( spies.server.firstCall.args[ 1 ] ).to.equal( 8888 ); - } ); - } ); + await runManualTests( { ...defaultOptions, ...options } ); - it( 'allows specifying identity file (absolute path)', () => { - spies.transformFileOptionToTestGlob.onFirstCall().returns( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js' ] ); - spies.transformFileOptionToTestGlob.onSecondCall().returns( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); + expect( vi.mocked( createManualTestServer ) ).toHaveBeenCalledExactlyOnceWith( + expect.any( String ), + 8888, + expect.any( Function ) + ); + } ); + it( 'allows specifying identity file (an absolute path)', async () => { const options = { files: [ 'ckeditor5-classic', @@ -390,36 +295,14 @@ describe( 'runManualTests', () => { identityFile: '/absolute/path/to/secrets.js' }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - themePath: null, - language: undefined, - tsconfig: undefined, - onTestCompilationStatus: sinon.match.func, - debug: undefined, - additionalLanguages: undefined, - disableWatch: false, - identityFile: '/absolute/path/to/secrets.js' - } ); - } ); - } ); + await runManualTests( { ...defaultOptions, ...options } ); - it( 'allows specifying identity file (relative path)', () => { - spies.transformFileOptionToTestGlob.onFirstCall().returns( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js' ] ); - spies.transformFileOptionToTestGlob.onSecondCall().returns( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + identityFile: '/absolute/path/to/secrets.js' + } ) ); + } ); + it( 'allows specifying identity file (a relative path)', async () => { const options = { files: [ 'ckeditor5-classic', @@ -428,34 +311,15 @@ describe( 'runManualTests', () => { identityFile: 'path/to/secrets.js' }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - themePath: null, - language: undefined, - tsconfig: undefined, - onTestCompilationStatus: sinon.match.func, - debug: undefined, - additionalLanguages: undefined, - disableWatch: false, - identityFile: 'path/to/secrets.js' - } ); - } ); + await runManualTests( { ...defaultOptions, ...options } ); + + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + identityFile: 'path/to/secrets.js' + } ) ); } ); - it( 'should allow hiding processed files in the console', () => { - spies.transformFileOptionToTestGlob.returns( [ + it( 'should allow hiding processed files in the console', async () => { + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); @@ -464,55 +328,18 @@ describe( 'runManualTests', () => { silent: true }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.removeDir.calledOnce ).to.equal( true ); - expect( spies.removeDir.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - expect( spies.removeDir.firstCall.args[ 1 ] ).to.deep.equal( { silent: true } ); - - expect( spies.htmlFileCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.htmlFileCompiler.firstCall, { - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - language: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - disableWatch: true, - silent: true - } ); + await runManualTests( { ...defaultOptions, ...options } ); - expect( spies.scriptCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - themePath: null, - language: undefined, - tsconfig: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - debug: undefined, - disableWatch: true, - identityFile: undefined - } ); - - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - } ); + expect( vi.mocked( removeDir ) ).toHaveBeenCalledExactlyOnceWith( expect.any( String ), expect.objectContaining( { + silent: true + } ) ); + expect( vi.mocked( compileManualTestHtmlFiles ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + silent: true + } ) ); } ); - it( 'should allow disabling listening for changes in source files', () => { - spies.transformFileOptionToTestGlob.returns( [ + it( 'should allow disabling listening for changes in source files', async () => { + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); @@ -521,51 +348,18 @@ describe( 'runManualTests', () => { disableWatch: true }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.htmlFileCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.htmlFileCompiler.firstCall, { - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - language: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - disableWatch: true, - silent: false - } ); + await runManualTests( { ...defaultOptions, ...options } ); - expect( spies.scriptCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - themePath: null, - language: undefined, - tsconfig: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - debug: undefined, - disableWatch: true, - identityFile: undefined - } ); - - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - } ); + expect( vi.mocked( compileManualTestHtmlFiles ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + disableWatch: true + } ) ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + disableWatch: true + } ) ); } ); - it( 'compiles only manual test files (ignores utils and files outside the manual directory)', () => { - spies.transformFileOptionToTestGlob.returns( [ + it( 'compiles only manual test files (ignores utils and files outside the manual directory)', async () => { + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', 'workspace/packages/ckeditor-*/tests/**/manual/_utils/**/*.js', 'workspace/packages/ckeditor-*/tests/**/outside/**/*.js' @@ -575,70 +369,49 @@ describe( 'runManualTests', () => { disableWatch: true }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.htmlFileCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.htmlFileCompiler.firstCall, { - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - language: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - disableWatch: true, - silent: false - } ); - - expect( spies.scriptCompiler.calledOnce ).to.equal( true ); - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - themePath: null, - language: undefined, - tsconfig: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - debug: undefined, - disableWatch: true, - identityFile: undefined - } ); + await runManualTests( { ...defaultOptions, ...options } ); - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - } ); + expect( vi.mocked( compileManualTestHtmlFiles ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + sourceFiles: [ + 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', + 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' + ] + } ) ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + sourceFiles: [ + 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', + 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' + ] + } ) ); } ); - it( 'should start a socket.io server as soon as the http server is up and running', () => { - spies.transformFileOptionToTestGlob.returns( [ + it( 'should start a socket.io server as soon as the http server is up and running', async () => { + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); - const httpServerMock = sinon.spy(); + const httpServerMock = vi.fn(); - spies.server.callsFake( ( buildDire, port, onCreate ) => onCreate( httpServerMock ) ); + vi.mocked( createManualTestServer ).mockImplementation( ( buildDire, port, onCreate ) => onCreate( httpServerMock ) ); - return runManualTests( defaultOptions ) - .then( () => { - sinon.assert.calledOnce( spies.socketIO.Server ); - sinon.assert.calledWithExactly( spies.socketIO.Server, httpServerMock ); - } ); + await runManualTests( defaultOptions ); + + expect( vi.mocked( Server ) ).toHaveBeenCalledExactlyOnceWith( httpServerMock ); } ); - it( 'should set disableWatch to true if files flag is not provided', () => { - return runManualTests( defaultOptions ) - .then( () => { - sinon.assert.calledWith( spies.scriptCompiler.firstCall, sinon.match.has( 'disableWatch', true ) ); - } ); + it( 'should set disableWatch to true if files flag is not provided', async () => { + await runManualTests( defaultOptions ); + + expect( vi.mocked( compileManualTestHtmlFiles ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + disableWatch: true + } ) ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + disableWatch: true + } ) ); } ); - it( 'should set disableWatch to false if files flag is provided', () => { + it( 'should set disableWatch to false if files flag is provided', async () => { const options = { files: [ 'ckeditor5-classic', @@ -646,13 +419,17 @@ describe( 'runManualTests', () => { ] }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - sinon.assert.calledWith( spies.scriptCompiler.firstCall, sinon.match.has( 'disableWatch', false ) ); - } ); + await runManualTests( { ...defaultOptions, ...options } ); + + expect( vi.mocked( compileManualTestHtmlFiles ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + disableWatch: false + } ) ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + disableWatch: false + } ) ); } ); - it( 'should read disableWatch flag value even if files flag is provided', () => { + it( 'should read disableWatch flag value even if files flag is provided', async () => { const options = { files: [ 'ckeditor5-classic', @@ -661,563 +438,374 @@ describe( 'runManualTests', () => { disableWatch: true }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - sinon.assert.calledWith( spies.scriptCompiler.firstCall, sinon.match.has( 'disableWatch', true ) ); - } ); - } ); + await runManualTests( { ...defaultOptions, ...options } ); - it( 'should set default values for files', () => { - return runManualTests( defaultOptions ) - .then( () => { - expect( spies.transformFileOptionToTestGlob.calledTwice ).to.equal( true ); - expect( spies.transformFileOptionToTestGlob.firstCall.args[ 0 ] ).to.equal( '*' ); - expect( spies.transformFileOptionToTestGlob.secondCall.args[ 0 ] ).to.equal( 'ckeditor5' ); - } ); + expect( vi.mocked( compileManualTestHtmlFiles ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + disableWatch: true + } ) ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + disableWatch: true + } ) ); } ); - it( 'should transform provided files to glob', () => { - const options = { - files: [ - 'ckeditor5-classic', - 'ckeditor-classic/manual/classic.js' - ] - }; + it( 'should find all CKEditor 5 manual tests if the `files` option is not defined', async () => { + await runManualTests( defaultOptions ); - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.transformFileOptionToTestGlob.calledTwice ).to.equal( true ); - expect( spies.transformFileOptionToTestGlob.firstCall.args[ 0 ] ).to.equal( 'ckeditor5-classic' ); - expect( spies.transformFileOptionToTestGlob.secondCall.args[ 0 ] ).to.equal( 'ckeditor-classic/manual/classic.js' ); - } ); + expect( vi.mocked( transformFileOptionToTestGlob ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( transformFileOptionToTestGlob ) ).toHaveBeenCalledWith( '*', true ); + expect( vi.mocked( transformFileOptionToTestGlob ) ).toHaveBeenCalledWith( 'ckeditor5', true ); } ); - it( 'should not duplicate glob files in the final sourceFiles array', () => { - spies.transformFileOptionToTestGlob.returns( [ + it( 'should not duplicate glob files in the final sourceFiles array', async () => { + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); - return runManualTests( defaultOptions ) - .then( () => { - expect( spies.scriptCompiler.firstCall.args[ 0 ].sourceFiles ).to.deep.equal( [ - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ] ); - } ); + await runManualTests( defaultOptions ); + + expect( vi.mocked( transformFileOptionToTestGlob ) ).toHaveBeenCalledTimes( 2 ); + + expect( vi.mocked( compileManualTestHtmlFiles ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + sourceFiles: [ + 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', + 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' + ] + } ) ); + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + sourceFiles: [ + 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', + 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' + ] + } ) ); } ); describe( 'DLLs', () => { - it( 'should not build the DLLs if there are no DLL-related manual tests', () => { - spies.transformFileOptionToTestGlob.returns( [ - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' - ] ); + beforeEach( () => { + vi.mocked( isInteractive ).mockReturnValue( true ); - return runManualTests( defaultOptions ) - .then( () => { - sinon.assert.notCalled( spies.childProcess.spawn ); - sinon.assert.notCalled( spies.devUtils.logInfo ); - sinon.assert.notCalled( spies.devUtils.logWarning ); - sinon.assert.notCalled( spies.inquirer.prompt ); - sinon.assert.notCalled( spies.path.resolve ); - sinon.assert.notCalled( spies.fs.readFileSync ); - } ); - } ); - - it( 'should not build the DLLs if the console is not interactive', () => { - spies.isInteractive.returns( false ); - spies.transformFileOptionToTestGlob.returns( [ + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' ] ); - - return runManualTests( defaultOptions ) - .then( () => { - sinon.assert.notCalled( spies.childProcess.spawn ); - sinon.assert.notCalled( spies.devUtils.logInfo ); - sinon.assert.notCalled( spies.devUtils.logWarning ); - sinon.assert.notCalled( spies.inquirer.prompt ); - sinon.assert.notCalled( spies.path.resolve ); - sinon.assert.notCalled( spies.fs.readFileSync ); - } ); } ); - it( 'should not build the DLLs and not ask user if `--dll` flag is `false`, even if console is interactive', () => { - spies.isInteractive.returns( true ); - spies.transformFileOptionToTestGlob.returns( [ + it( 'should not build the DLLs if there are no DLL-related manual tests', async () => { + vi.mocked( transformFileOptionToTestGlob ).mockReturnValue( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' + 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); + await runManualTests( defaultOptions ); + + expect( vi.mocked( spawn ) ).not.toHaveBeenCalled(); + expect( vi.mocked( inquirer ).prompt ).not.toHaveBeenCalled(); + expect( stubs.log.info ).not.toHaveBeenCalled(); + expect( stubs.log.warning ).not.toHaveBeenCalled(); + } ); + + it( 'should not build the DLLs if the console is not interactive', async () => { + vi.mocked( isInteractive ).mockReturnValue( false ); + + await runManualTests( defaultOptions ); + + expect( vi.mocked( spawn ) ).not.toHaveBeenCalled(); + expect( vi.mocked( inquirer ).prompt ).not.toHaveBeenCalled(); + expect( stubs.log.info ).not.toHaveBeenCalled(); + expect( stubs.log.warning ).not.toHaveBeenCalled(); + } ); + + it( 'should not build the DLLs and not ask user if `--dll` flag is `false`, even if console is interactive', async () => { const options = { dll: false }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - sinon.assert.notCalled( spies.childProcess.spawn ); - sinon.assert.notCalled( spies.devUtils.logInfo ); - sinon.assert.notCalled( spies.devUtils.logWarning ); - sinon.assert.notCalled( spies.inquirer.prompt ); - sinon.assert.notCalled( spies.path.resolve ); - sinon.assert.notCalled( spies.fs.readFileSync ); - } ); + await runManualTests( { ...defaultOptions, ...options } ); + + expect( vi.mocked( spawn ) ).not.toHaveBeenCalled(); + expect( vi.mocked( inquirer ).prompt ).not.toHaveBeenCalled(); + expect( stubs.log.info ).not.toHaveBeenCalled(); + expect( stubs.log.warning ).not.toHaveBeenCalled(); } ); - it( 'should not build the DLLs if user declined the question', () => { - spies.isInteractive.returns( true ); - spies.inquirer.prompt.resolves( { confirm: false } ); - spies.transformFileOptionToTestGlob.returns( [ - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' - ] ); + it( 'should not build the DLLs if user declined the question', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { confirm: false } ); - return runManualTests( defaultOptions ) - .then( () => { - sinon.assert.notCalled( spies.childProcess.spawn ); - - sinon.assert.calledOnce( spies.devUtils.logWarning ); - sinon.assert.calledWith( spies.devUtils.logWarning.firstCall, - '\n⚠ Some tests require DLL builds.\n' - ); - - sinon.assert.calledTwice( spies.devUtils.logInfo ); - sinon.assert.calledWith( spies.devUtils.logInfo.firstCall, - 'You don\'t have to update these builds every time unless you want to check changes in DLL tests.' - ); - sinon.assert.calledWith( spies.devUtils.logInfo.secondCall, - 'You can use the following flags to skip this prompt in the future: --dll / --no-dll.\n' - ); - - sinon.assert.calledOnce( spies.inquirer.prompt ); - sinon.assert.calledWith( spies.inquirer.prompt.firstCall, [ { - message: 'Create the DLL builds now?', - type: 'confirm', - name: 'confirm', - default: false - } ] ); - - sinon.assert.notCalled( spies.path.resolve ); - sinon.assert.notCalled( spies.fs.readFileSync ); - } ); + await runManualTests( defaultOptions ); + + expect( vi.mocked( spawn ) ).not.toHaveBeenCalled(); + expect( vi.mocked( inquirer ).prompt ).toHaveBeenCalledExactlyOnceWith( [ + { + message: 'Create the DLL builds now?', + type: 'confirm', + name: 'confirm', + default: false + } + ] ); + expect( stubs.log.warning ).toHaveBeenCalledExactlyOnceWith( '\n⚠ Some tests require DLL builds.\n' ); + expect( stubs.log.info ).toHaveBeenCalledTimes( 2 ); + expect( stubs.log.info ).toHaveBeenCalledWith( + 'You don\'t have to update these builds every time unless you want to check changes in DLL tests.' + ); + expect( stubs.log.info ).toHaveBeenCalledWith( + 'You can use the following flags to skip this prompt in the future: --dll / --no-dll.\n' + ); + expect( vi.mocked( chalk ).bold ).toHaveBeenCalledTimes( 3 ); } ); - it( 'should open the package.json in each repository in proper order (root repository first, then external ones)', () => { - spies.isInteractive.returns( true ); - spies.inquirer.prompt.resolves( { confirm: true } ); - spies.fs.readFileSync.returns( JSON.stringify( { + it( 'should open the package.json in each repository in proper order (root repository first, then external ones)', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { confirm: true } ); + vi.mocked( fs ).readFileSync.mockReturnValue( JSON.stringify( { name: 'ckeditor5-example-package' } ) ); - spies.transformFileOptionToTestGlob.returns( [ - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' - ] ); - const consoleStub = sinon.stub( console, 'log' ); - - return runManualTests( defaultOptions ) - .then( () => { - consoleStub.restore(); - - expect( consoleStub.callCount ).to.equal( 1 ); - expect( consoleStub.firstCall.firstArg ).to.equal( '\n📍 DLL building complete.\n' ); - - sinon.assert.notCalled( spies.childProcess.spawn ); - - sinon.assert.calledOnce( spies.devUtils.logWarning ); - sinon.assert.calledWith( spies.devUtils.logWarning.firstCall, - '\n⚠ Some tests require DLL builds.\n' - ); - - sinon.assert.calledTwice( spies.devUtils.logInfo ); - sinon.assert.calledWith( spies.devUtils.logInfo.firstCall, - 'You don\'t have to update these builds every time unless you want to check changes in DLL tests.' - ); - sinon.assert.calledWith( spies.devUtils.logInfo.secondCall, - 'You can use the following flags to skip this prompt in the future: --dll / --no-dll.\n' - ); - - sinon.assert.calledOnce( spies.inquirer.prompt ); - sinon.assert.calledWith( spies.inquirer.prompt.firstCall, [ { - message: 'Create the DLL builds now?', - type: 'confirm', - name: 'confirm', - default: false - } ] ); - - // The `path.resolve()` calls are not sorted, so it is called in the same order as data returned from `glob`. - sinon.assert.calledThrice( spies.path.resolve ); - sinon.assert.calledWith( spies.path.resolve.firstCall, - 'workspace/ckeditor5/external/ckeditor5-internal/package.json' - ); - sinon.assert.calledWith( spies.path.resolve.secondCall, - 'workspace/ckeditor5/external/collaboration-features/package.json' - ); - sinon.assert.calledWith( spies.path.resolve.thirdCall, - 'workspace/ckeditor5/package.json' - ); - - // The `fs.readFileSync()` calls are sorted: root repository first, then external ones. - sinon.assert.calledThrice( spies.fs.readFileSync ); - sinon.assert.calledWith( spies.fs.readFileSync.firstCall, - '/absolute/path/to/workspace/ckeditor5/package.json' - ); - sinon.assert.calledWith( spies.fs.readFileSync.secondCall, - '/absolute/path/to/workspace/ckeditor5/external/ckeditor5-internal/package.json' - ); - sinon.assert.calledWith( spies.fs.readFileSync.thirdCall, - '/absolute/path/to/workspace/ckeditor5/external/collaboration-features/package.json' - ); - } ); + const consoleStub = vi.spyOn( console, 'log' ).mockImplementation( () => {} ); + + await runManualTests( defaultOptions ); + + expect( consoleStub ).toHaveBeenCalledExactlyOnceWith( '\n📍 DLL building complete.\n' ); + consoleStub.mockRestore(); + + // The `path.resolve()` calls are not sorted, so it is called in the same order as data returned from `glob`. + expect( vi.mocked( path ).resolve ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( path ).resolve ).toHaveBeenCalledWith( 'workspace/ckeditor5/external/ckeditor5-internal/package.json' ); + expect( vi.mocked( path ).resolve ).toHaveBeenCalledWith( 'workspace/ckeditor5/external/collaboration-features/package.json' ); + expect( vi.mocked( path ).resolve ).toHaveBeenCalledWith( 'workspace/ckeditor5/package.json' ); + + // The `fs.readFileSync()` calls are sorted: root repository first, then external ones. + expect( vi.mocked( fs ).readFileSync ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs ).readFileSync ).toHaveBeenNthCalledWith( + 1, + '/absolute/path/to/workspace/ckeditor5/package.json', + 'utf-8' + ); + + expect( vi.mocked( fs ).readFileSync ).toHaveBeenNthCalledWith( + 2, + '/absolute/path/to/workspace/ckeditor5/external/ckeditor5-internal/package.json', + 'utf-8' + ); + expect( vi.mocked( fs ).readFileSync ).toHaveBeenNthCalledWith( + 3, + '/absolute/path/to/workspace/ckeditor5/external/collaboration-features/package.json', + 'utf-8' + ); } ); - it( 'should not build the DLLs if no repository has scripts in package.json', () => { - spies.isInteractive.returns( true ); - spies.inquirer.prompt.resolves( { confirm: true } ); - spies.fs.readFileSync.returns( JSON.stringify( { + it( 'should not build the DLLs if no repository has scripts in package.json', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { confirm: true } ); + vi.mocked( fs ).readFileSync.mockReturnValue( JSON.stringify( { name: 'ckeditor5-example-package' } ) ); - spies.transformFileOptionToTestGlob.returns( [ - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' - ] ); - - const consoleStub = sinon.stub( console, 'log' ); - return runManualTests( defaultOptions ) - .then( () => { - consoleStub.restore(); + vi.spyOn( console, 'log' ).mockImplementation( () => {} ); - expect( consoleStub.callCount ).to.equal( 1 ); - expect( consoleStub.firstCall.firstArg ).to.equal( '\n📍 DLL building complete.\n' ); + await runManualTests( defaultOptions ); - sinon.assert.notCalled( spies.childProcess.spawn ); - } ); + expect( vi.mocked( spawn ) ).not.toHaveBeenCalled(); } ); - it( 'should not build the DLLs if no repository has script to build DLLs in package.json', () => { - spies.isInteractive.returns( true ); - spies.inquirer.prompt.resolves( { confirm: true } ); - spies.fs.readFileSync.returns( JSON.stringify( { + it( 'should not build the DLLs if no repository has script to build DLLs in package.json', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { confirm: true } ); + vi.mocked( fs ).readFileSync.mockReturnValue( JSON.stringify( { name: 'ckeditor5-example-package', scripts: { 'build': 'node ./scripts/build' } } ) ); - spies.transformFileOptionToTestGlob.returns( [ - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' - ] ); - const consoleStub = sinon.stub( console, 'log' ); + vi.spyOn( console, 'log' ).mockImplementation( () => {} ); - return runManualTests( defaultOptions ) - .then( () => { - consoleStub.restore(); + await runManualTests( defaultOptions ); - expect( consoleStub.callCount ).to.equal( 1 ); - expect( consoleStub.firstCall.firstArg ).to.equal( '\n📍 DLL building complete.\n' ); - - sinon.assert.notCalled( spies.childProcess.spawn ); - } ); + expect( vi.mocked( spawn ) ).not.toHaveBeenCalled(); } ); - it( 'should build the DLLs in each repository that has script to build DLLs in package.json', () => { - spies.isInteractive.returns( true ); - spies.inquirer.prompt.resolves( { confirm: true } ); - spies.fs.readFileSync - .returns( JSON.stringify( { + it( 'should build the DLLs in each repository that has script to build DLLs in package.json', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { confirm: true } ); + vi.mocked( fs ).readFileSync.mockImplementation( input => { + if ( input === '/absolute/path/to/workspace/ckeditor5/external/collaboration-features/package.json' ) { + return JSON.stringify( { + name: 'ckeditor5-example-package', + scripts: { + 'build': 'node ./scripts/build' + } + } ); + } + + return JSON.stringify( { name: 'ckeditor5-example-package', scripts: { 'dll:build': 'node ./scripts/build-dll' } - } ) ) - .withArgs( '/absolute/path/to/workspace/ckeditor5/external/collaboration-features/package.json' ) - .returns( JSON.stringify( { - name: 'ckeditor5-example-package', - scripts: { - 'build': 'node ./scripts/build' - } - } ) ); - spies.transformFileOptionToTestGlob.returns( [ - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' - ] ); - - const consoleStub = sinon.stub( console, 'log' ); + } ); + } ); - return runManualTests( defaultOptions ) - .then( () => { - consoleStub.restore(); + vi.spyOn( console, 'log' ).mockImplementation( () => {} ); - expect( consoleStub.callCount ).to.equal( 1 ); - expect( consoleStub.firstCall.firstArg ).to.equal( '\n📍 DLL building complete.\n' ); + await runManualTests( defaultOptions ); - sinon.assert.calledTwice( spies.childProcess.spawn ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledWith( + 'yarnpkg', + [ 'run', 'dll:build' ], + { + encoding: 'utf8', + shell: true, + cwd: '/absolute/path/to/workspace/ckeditor5', + stdio: 'inherit' + } + ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledWith( + 'yarnpkg', + [ 'run', 'dll:build' ], + { + encoding: 'utf8', + shell: true, + cwd: '/absolute/path/to/workspace/ckeditor5/external/ckeditor5-internal', + stdio: 'inherit' + } + ); + } ); - sinon.assert.calledWith( spies.childProcess.spawn.firstCall, - 'yarnpkg', - [ 'run', 'dll:build' ], - { - encoding: 'utf8', - shell: true, - cwd: '/absolute/path/to/workspace/ckeditor5', - stdio: 'inherit' - } - ); - sinon.assert.calledWith( spies.childProcess.spawn.secondCall, - 'yarnpkg', - [ 'run', 'dll:build' ], - { - encoding: 'utf8', - shell: true, - cwd: '/absolute/path/to/workspace/ckeditor5/external/ckeditor5-internal', - stdio: 'inherit' + it( 'should build the DLLs automatically and not ask user if `--dll` flag is `true`, even if console is interactive', async () => { + vi.mocked( fs ).readFileSync.mockImplementation( input => { + if ( input === '/absolute/path/to/workspace/ckeditor5/external/collaboration-features/package.json' ) { + return JSON.stringify( { + name: 'ckeditor5-example-package', + scripts: { + 'build': 'node ./scripts/build' } - ); - - sinon.assert.calledOnce( spies.devUtils.logWarning ); - sinon.assert.calledWith( spies.devUtils.logWarning.firstCall, - '\n⚠ Some tests require DLL builds.\n' - ); - - sinon.assert.callCount( spies.devUtils.logInfo, 4 ); - sinon.assert.calledWith( spies.devUtils.logInfo.getCall( 0 ), - 'You don\'t have to update these builds every time unless you want to check changes in DLL tests.' - ); - sinon.assert.calledWith( spies.devUtils.logInfo.getCall( 1 ), - 'You can use the following flags to skip this prompt in the future: --dll / --no-dll.\n' - ); - sinon.assert.calledWith( spies.devUtils.logInfo.getCall( 2 ), - '\n📍 Building DLLs in ckeditor5...\n' - ); - sinon.assert.calledWith( spies.devUtils.logInfo.getCall( 3 ), - '\n📍 Building DLLs in ckeditor5-internal...\n' - ); - } ); - } ); + } ); + } - it( 'should build the DLLs automatically and not ask user if `--dll` flag is `true`, even if console is interactive', () => { - spies.isInteractive.returns( true ); - spies.fs.readFileSync - .returns( JSON.stringify( { + return JSON.stringify( { name: 'ckeditor5-example-package', scripts: { 'dll:build': 'node ./scripts/build-dll' } - } ) ) - .withArgs( '/absolute/path/to/workspace/ckeditor5/external/collaboration-features/package.json' ) - .returns( JSON.stringify( { - name: 'ckeditor5-example-package', - scripts: { - 'build': 'node ./scripts/build' - } - } ) ); - spies.transformFileOptionToTestGlob.returns( [ - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' - ] ); + } ); + } ); const options = { dll: true }; - const consoleStub = sinon.stub( console, 'log' ); - - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - consoleStub.restore(); + vi.spyOn( console, 'log' ).mockImplementation( () => {} ); - expect( consoleStub.callCount ).to.equal( 1 ); - expect( consoleStub.firstCall.firstArg ).to.equal( '\n📍 DLL building complete.\n' ); + await runManualTests( { ...defaultOptions, ...options } ); - sinon.assert.notCalled( spies.inquirer.prompt ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledWith( + 'yarnpkg', + [ 'run', 'dll:build' ], + { + encoding: 'utf8', + shell: true, + cwd: '/absolute/path/to/workspace/ckeditor5', + stdio: 'inherit' + } + ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledWith( + 'yarnpkg', + [ 'run', 'dll:build' ], + { + encoding: 'utf8', + shell: true, + cwd: '/absolute/path/to/workspace/ckeditor5/external/ckeditor5-internal', + stdio: 'inherit' + } + ); + } ); - sinon.assert.calledTwice( spies.childProcess.spawn ); - sinon.assert.calledWith( spies.childProcess.spawn.firstCall, - 'yarnpkg', - [ 'run', 'dll:build' ], - { - encoding: 'utf8', - shell: true, - cwd: '/absolute/path/to/workspace/ckeditor5', - stdio: 'inherit' + it( 'should reject a promise if building DLLs has failed', async () => { + vi.mocked( inquirer ).prompt.mockResolvedValue( { confirm: true } ); + vi.mocked( fs ).readFileSync.mockImplementation( input => { + if ( input === '/absolute/path/to/workspace/ckeditor5/external/collaboration-features/package.json' ) { + return JSON.stringify( { + name: 'ckeditor5-example-package', + scripts: { + 'build': 'node ./scripts/build' } - ); - sinon.assert.calledWith( spies.childProcess.spawn.secondCall, - 'yarnpkg', - [ 'run', 'dll:build' ], - { - encoding: 'utf8', - shell: true, - cwd: '/absolute/path/to/workspace/ckeditor5/external/ckeditor5-internal', - stdio: 'inherit' - } - ); - - sinon.assert.notCalled( spies.devUtils.logWarning ); + } ); + } - sinon.assert.calledTwice( spies.devUtils.logInfo ); - sinon.assert.calledWith( spies.devUtils.logInfo.firstCall, - '\n📍 Building DLLs in ckeditor5...\n' - ); - sinon.assert.calledWith( spies.devUtils.logInfo.secondCall, - '\n📍 Building DLLs in ckeditor5-internal...\n' - ); + return JSON.stringify( { + name: 'ckeditor5-example-package', + scripts: { + 'dll:build': 'node ./scripts/build-dll' + } } ); - } ); - - it( 'should reject a promise if building DLLs has failed', () => { - spies.isInteractive.returns( true ); - spies.inquirer.prompt.resolves( { confirm: true } ); - spies.fs.readFileSync.returns( JSON.stringify( { - name: 'ckeditor5-example-package', - scripts: { - 'dll:build': 'node ./scripts/build-dll' + } ); + stubs.spawn.spawnExitCode = 1; + + await expect( runManualTests( defaultOptions ) ) + .rejects.toThrow( 'Building DLLs in ckeditor5 finished with an error.' ); + + expect( vi.mocked( spawn ) ).toHaveBeenCalledExactlyOnceWith( + 'yarnpkg', + [ 'run', 'dll:build' ], + { + encoding: 'utf8', + shell: true, + cwd: '/absolute/path/to/workspace/ckeditor5', + stdio: 'inherit' } - } ) ); - spies.transformFileOptionToTestGlob.returns( [ - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' - ] ); - spies.childProcess.spawnExitCode = 1; - - return runManualTests( defaultOptions ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - error => { - expect( error.message ).to.equal( 'Building DLLs in ckeditor5 finished with an error.' ); - - sinon.assert.calledOnce( spies.childProcess.spawn ); - sinon.assert.calledWith( spies.childProcess.spawn.firstCall, - 'yarnpkg', - [ 'run', 'dll:build' ], - { - encoding: 'utf8', - shell: true, - cwd: '/absolute/path/to/workspace/ckeditor5', - stdio: 'inherit' - } - ); - } - ); + ); } ); - it( 'should build the DLLs in each repository for Windows environment', () => { - sandbox.stub( process, 'platform' ).value( 'win32' ); + it( 'should build the DLLs in each repository for Windows environment', async () => { + vi.spyOn( process, 'platform', 'get' ).mockReturnValue( 'win32' ); - spies.isInteractive.returns( true ); - spies.inquirer.prompt.resolves( { confirm: true } ); - spies.fs.readFileSync.returns( JSON.stringify( { + vi.mocked( inquirer ).prompt.mockResolvedValue( { confirm: true } ); + vi.mocked( fs ).readFileSync.mockReturnValue( JSON.stringify( { name: 'ckeditor5-example-package', scripts: { 'dll:build': 'node ./scripts/build-dll' } } ) ); - spies.transformFileOptionToTestGlob.returns( [ - 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js', - 'workspace/packages/ckeditor5-*/tests/**/manual/dll/**/*.js' - ] ); - - const consoleStub = sinon.stub( console, 'log' ); - - return runManualTests( defaultOptions ) - .then( () => { - consoleStub.restore(); - - expect( consoleStub.callCount ).to.equal( 1 ); - expect( consoleStub.firstCall.firstArg ).to.equal( '\n📍 DLL building complete.\n' ); - - sinon.assert.calledOnce( spies.scriptCompiler ); - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace\\packages\\ckeditor5-foo\\tests\\manual\\feature-a.js', - 'workspace\\packages\\ckeditor5-bar\\tests\\manual\\feature-b.js', - 'workspace\\packages\\ckeditor-foo\\tests\\manual\\feature-c.js', - 'workspace\\packages\\ckeditor-bar\\tests\\manual\\feature-d.js', - 'workspace\\packages\\ckeditor5-foo\\tests\\manual\\dll\\feature-i-dll.js', - 'workspace\\packages\\ckeditor5-bar\\tests\\manual\\dll\\feature-j-dll.js' - ], - themePath: null, - language: undefined, - tsconfig: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - debug: undefined, - disableWatch: true, - identityFile: undefined - } ); - sinon.assert.calledOnce( spies.htmlFileCompiler ); - sinon.assert.calledWith( spies.htmlFileCompiler.firstCall, { - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace\\packages\\ckeditor5-foo\\tests\\manual\\feature-a.js', - 'workspace\\packages\\ckeditor5-bar\\tests\\manual\\feature-b.js', - 'workspace\\packages\\ckeditor-foo\\tests\\manual\\feature-c.js', - 'workspace\\packages\\ckeditor-bar\\tests\\manual\\feature-d.js', - 'workspace\\packages\\ckeditor5-foo\\tests\\manual\\dll\\feature-i-dll.js', - 'workspace\\packages\\ckeditor5-bar\\tests\\manual\\dll\\feature-j-dll.js' - ], - language: undefined, - onTestCompilationStatus: sinon.match.func, - additionalLanguages: undefined, - disableWatch: true, - silent: false - } ); - - sinon.assert.calledThrice( spies.childProcess.spawn ); - sinon.assert.calledWith( spies.childProcess.spawn.firstCall, - 'yarnpkg', - [ 'run', 'dll:build' ], - { - encoding: 'utf8', - shell: true, - cwd: '/absolute/path/to/workspace/ckeditor5', - stdio: 'inherit' - } - ); - sinon.assert.calledWith( spies.childProcess.spawn.secondCall, - 'yarnpkg', - [ 'run', 'dll:build' ], - { - encoding: 'utf8', - shell: true, - cwd: '/absolute/path/to/workspace/ckeditor5/external/ckeditor5-internal', - stdio: 'inherit' - } - ); - sinon.assert.calledWith( spies.childProcess.spawn.thirdCall, - 'yarnpkg', - [ 'run', 'dll:build' ], - { - encoding: 'utf8', - shell: true, - cwd: '/absolute/path/to/workspace/ckeditor5/external/collaboration-features', - stdio: 'inherit' - } - ); - } ); + vi.spyOn( console, 'log' ).mockImplementation( () => {} ); + + await runManualTests( defaultOptions ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledWith( + 'yarnpkg', + [ 'run', 'dll:build' ], + { + encoding: 'utf8', + shell: true, + cwd: '/absolute/path/to/workspace/ckeditor5', + stdio: 'inherit' + } + ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledWith( + 'yarnpkg', + [ 'run', 'dll:build' ], + { + encoding: 'utf8', + shell: true, + cwd: '/absolute/path/to/workspace/ckeditor5/external/ckeditor5-internal', + stdio: 'inherit' + } + ); + expect( vi.mocked( spawn ) ).toHaveBeenCalledWith( + 'yarnpkg', + [ 'run', 'dll:build' ], + { + encoding: 'utf8', + shell: true, + cwd: '/absolute/path/to/workspace/ckeditor5/external/collaboration-features', + stdio: 'inherit' + } + ); } ); - it( 'allows specifying tsconfig file', () => { - spies.transformFileOptionToTestGlob.onFirstCall().returns( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js' ] ); - spies.transformFileOptionToTestGlob.onSecondCall().returns( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); + it( 'allows specifying tsconfig file', async () => { + vi.mocked( transformFileOptionToTestGlob ) + .mockReturnValueOnce( [ 'workspace/packages/ckeditor5-*/tests/**/manual/**/*.js' ] ) + .mockReturnValueOnce( [ 'workspace/packages/ckeditor-*/tests/**/manual/**/*.js' ] ); const options = { files: [ @@ -1227,30 +815,11 @@ describe( 'runManualTests', () => { tsconfig: '/absolute/path/to/tsconfig.json' }; - return runManualTests( { ...defaultOptions, ...options } ) - .then( () => { - expect( spies.server.calledOnce ).to.equal( true ); - expect( spies.server.firstCall.args[ 0 ] ).to.equal( 'workspace/build/.manual-tests' ); - - sinon.assert.calledWith( spies.scriptCompiler.firstCall, { - cwd: 'workspace', - buildDir: 'workspace/build/.manual-tests', - sourceFiles: [ - 'workspace/packages/ckeditor5-foo/tests/manual/feature-a.js', - 'workspace/packages/ckeditor5-bar/tests/manual/feature-b.js', - 'workspace/packages/ckeditor-foo/tests/manual/feature-c.js', - 'workspace/packages/ckeditor-bar/tests/manual/feature-d.js' - ], - themePath: null, - language: undefined, - onTestCompilationStatus: sinon.match.func, - debug: undefined, - additionalLanguages: undefined, - disableWatch: false, - tsconfig: '/absolute/path/to/tsconfig.json', - identityFile: undefined - } ); - } ); + await runManualTests( { ...defaultOptions, ...options } ); + + expect( vi.mocked( compileManualTestScripts ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + tsconfig: '/absolute/path/to/tsconfig.json' + } ) ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/assertions/attribute.js b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/assertions/attribute.js index 99d4514b5..7684962e2 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/assertions/attribute.js +++ b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/assertions/attribute.js @@ -3,12 +3,11 @@ * For licensing, see LICENSE.md. */ -const chai = require( 'chai' ); -const expect = chai.expect; -const attributeFactory = require( '../../../../lib/utils/automated-tests/assertions/attribute' ); +import { beforeAll, describe, expect, it, chai } from 'vitest'; +import attributeFactory from '../../../../lib/utils/automated-tests/assertions/attribute.js'; describe( 'attribute chai assertion', () => { - before( () => { + beforeAll( () => { attributeFactory( chai ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/assertions/equal-markup.js b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/assertions/equal-markup.js index 302fc3b0c..de9bc8fa1 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/assertions/equal-markup.js +++ b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/assertions/equal-markup.js @@ -3,12 +3,11 @@ * For licensing, see LICENSE.md. */ -const chai = require( 'chai' ); -const expect = chai.expect; -const equalMarkupFactory = require( '../../../../lib/utils/automated-tests/assertions/equal-markup' ); +import { beforeAll, describe, expect, it, chai } from 'vitest'; +import equalMarkupFactory from '../../../../lib/utils/automated-tests/assertions/equal-markup.js'; describe( 'equalMarkup chai assertion', () => { - before( () => { + beforeAll( () => { equalMarkupFactory( chai ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/getkarmaconfig.js b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/getkarmaconfig.js index 975c18cc4..e028cf62b 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/getkarmaconfig.js +++ b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/getkarmaconfig.js @@ -3,49 +3,41 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const mockery = require( 'mockery' ); -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const path = require( 'path' ); +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import getWebpackConfigForAutomatedTests from '../../../lib/utils/automated-tests/getwebpackconfig.js'; +import getKarmaConfig from '../../../lib/utils/automated-tests/getkarmaconfig.js'; + +vi.mock( 'path', () => ( { + default: { + join: vi.fn( ( ...chunks ) => chunks.join( '/' ) ), + dirname: vi.fn() + } +} ) ); +vi.mock( '../../../lib/utils/automated-tests/getwebpackconfig.js' ); describe( 'getKarmaConfig()', () => { - let getKarmaConfig, sandbox, karmaConfigOverrides; - const originalEnv = process.env; + const karmaConfigOverrides = { + // A relative path according to the tested file. + // From: /ckeditor5-dev/packages/ckeditor5-dev-tests/lib/utils/automated-tests/getkarmaconfig.js + // To: /ckeditor5-dev/packages/ckeditor5-dev-tests/tests/utils/automated-tests/fixtures/karma-config-overrides/*.cjs + noop: '../../../tests/fixtures/karma-config-overrides/noop.cjs', + removeCoverage: '../../../tests/fixtures/karma-config-overrides/removecoverage.cjs' + }; beforeEach( () => { - sandbox = sinon.createSandbox(); - - karmaConfigOverrides = sandbox.spy(); - sandbox.stub( process, 'cwd' ).returns( 'workspace' ); - sandbox.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ); + vi.spyOn( process, 'cwd' ).mockReturnValue( 'workspace' ); - // Sinon cannot stub non-existing props. process.env = Object.assign( {}, originalEnv, { CI: false } ); - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( './getwebpackconfig', options => options ); - mockery.registerMock( 'karma-config-overrides', karmaConfigOverrides ); - - getKarmaConfig = require( '../../../lib/utils/automated-tests/getkarmaconfig' ); } ); afterEach( () => { - sandbox.restore(); - mockery.disable(); - mockery.deregisterAll(); - process.env = originalEnv; } ); it( 'should return basic karma config for all tested files', () => { + vi.mocked( getWebpackConfigForAutomatedTests ).mockReturnValue( { webpackConfig: true } ); + const options = { files: [ '*' ], reporter: 'mocha', @@ -63,16 +55,28 @@ describe( 'getKarmaConfig()', () => { const karmaConfig = getKarmaConfig( options ); - expect( karmaConfig ).to.have.own.property( 'basePath', 'workspace' ); - expect( karmaConfig ).to.have.own.property( 'frameworks' ); - expect( karmaConfig ).to.have.own.property( 'files' ); - expect( karmaConfig ).to.have.own.property( 'preprocessors' ); - expect( karmaConfig ).to.have.own.property( 'webpack' ); - expect( karmaConfig.webpack ).to.deep.equal( { ...options, files: [ 'workspace/packages/ckeditor5-*/tests/**/*.js' ] } ); - expect( karmaConfig ).to.have.own.property( 'webpackMiddleware' ); - expect( karmaConfig ).to.have.own.property( 'reporters' ); - expect( karmaConfig ).to.have.own.property( 'browsers' ); - expect( karmaConfig ).to.have.own.property( 'singleRun', true ); + expect( vi.mocked( getWebpackConfigForAutomatedTests ) ).toHaveBeenCalledExactlyOnceWith( { + ...options, + files: [ + 'workspace/packages/ckeditor5-*/tests/**/*.js' + ] + } ); + + expect( karmaConfig ).toEqual( expect.objectContaining( { + basePath: 'workspace', + frameworks: expect.any( Array ), + files: expect.any( Array ), + preprocessors: expect.any( Object ), + webpack: expect.any( Object ), + webpackMiddleware: expect.any( Object ), + reporters: expect.any( Array ), + browsers: expect.any( Array ), + singleRun: true + } ) ); + + expect( karmaConfig.webpack ).toEqual( expect.objectContaining( { + webpackConfig: true + } ) ); } ); // See: https://github.com/ckeditor/ckeditor5/issues/8823 @@ -92,15 +96,23 @@ describe( 'getKarmaConfig()', () => { } } ); - expect( karmaConfig ).to.have.own.property( 'proxies' ); - expect( karmaConfig.proxies ).to.have.own.property( '/assets/' ); - expect( karmaConfig.proxies ).to.have.own.property( '/example.com/image.png' ); - expect( karmaConfig.proxies ).to.have.own.property( '/www.example.com/image.png' ); - - expect( karmaConfig.files ).to.be.an( 'array' ); - expect( karmaConfig.files.length ).to.equal( 2 ); - expect( karmaConfig.files[ 0 ] ).to.equal( 'workspace/entry-file.js' ); - expect( karmaConfig.files[ 1 ].pattern ).to.equal( 'packages/ckeditor5-utils/tests/_assets/**/*' ); + expect( karmaConfig ).toEqual( expect.objectContaining( { + proxies: expect.any( Object ), + files: expect.any( Array ) + } ) ); + expect( karmaConfig.proxies ).toEqual( expect.objectContaining( { + '/assets/': expect.any( String ), + '/example.com/image.png': expect.any( String ), + '/www.example.com/image.png': expect.any( String ) + } ) ); + + expect( karmaConfig.files ).toHaveLength( 2 ); + expect( karmaConfig.files ).toEqual( expect.arrayContaining( [ + 'workspace/entry-file.js', + expect.objectContaining( { + pattern: 'packages/ckeditor5-utils/tests/_assets/**/*' + } ) + ] ) ); } ); it( 'should contain a list of available plugins', () => { @@ -119,50 +131,53 @@ describe( 'getKarmaConfig()', () => { } } ); - expect( karmaConfig.plugins ).to.be.an( 'array' ); - expect( karmaConfig.plugins ).to.have.lengthOf.above( 0 ); + expect( karmaConfig ).toEqual( expect.objectContaining( { + files: expect.any( Array ) + } ) ); + expect( karmaConfig.files ).not.toHaveLength( 0 ); } ); it( 'should enable webpack watcher when passed the "karmaConfigOverrides" option (execute in Intellij)', () => { + vi.mocked( getWebpackConfigForAutomatedTests ).mockReturnValue( { watch: null } ); + const karmaConfig = getKarmaConfig( { files: [ '*' ], reporter: 'mocha', - karmaConfigOverrides: 'karma-config-overrides', + karmaConfigOverrides: karmaConfigOverrides.noop, globPatterns: { '*': 'workspace/packages/ckeditor5-*/tests/**/*.js' } } ); - expect( karmaConfig.webpack ).to.contain.property( 'watch', true ); + expect( karmaConfig.webpack ).toEqual( expect.objectContaining( { + watch: true + } ) ); } ); it( 'should configure coverage reporter', () => { + vi.mocked( getWebpackConfigForAutomatedTests ).mockReturnValue( { } ); + const karmaConfig = getKarmaConfig( { files: [ '*' ], reporter: 'mocha', - karmaConfigOverrides: 'karma-config-overrides', + karmaConfigOverrides: karmaConfigOverrides.noop, globPatterns: { '*': 'workspace/packages/ckeditor5-*/tests/**/*.js' }, coverage: true } ); - expect( karmaConfig.reporters ).to.contain( 'coverage' ); - expect( karmaConfig.coverageReporter ).to.contain.property( 'reporters' ); + expect( karmaConfig ).toEqual( expect.objectContaining( { + reporters: expect.arrayContaining( [ 'coverage' ] ), + coverageReporter: { + reporters: expect.any( Array ), + watermarks: expect.any( Object ) + } + } ) ); } ); it( 'should remove webpack babel-loader if coverage reporter is removed by overrides', () => { - mockery.registerMock( 'karma-config-overrides-remove-coverage', config => { - config.reporters.splice( config.reporters.indexOf( 'coverage' ), 1 ); - } ); - - const karmaConfig = getKarmaConfig( { - files: [ '*' ], - reporter: 'mocha', - karmaConfigOverrides: 'karma-config-overrides-remove-coverage', - globPatterns: { - '*': 'workspace/packages/ckeditor5-*/tests/**/*.js' - }, + vi.mocked( getWebpackConfigForAutomatedTests ).mockReturnValue( { module: { rules: [ { @@ -172,20 +187,33 @@ describe( 'getKarmaConfig()', () => { loader: 'other-loader' } ] + } + } ); + + const karmaConfig = getKarmaConfig( { + files: [ '*' ], + reporter: 'mocha', + karmaConfigOverrides: karmaConfigOverrides.removeCoverage, + globPatterns: { + '*': 'workspace/packages/ckeditor5-*/tests/**/*.js' }, coverage: true } ); const loaders = karmaConfig.webpack.module.rules.map( rule => rule.loader ); - expect( karmaConfig.reporters ).to.not.contain( 'coverage' ); - expect( loaders ).to.not.contain( 'babel-loader' ); - expect( loaders ).to.contain( 'other-loader' ); + expect( karmaConfig ).not.toEqual( expect.objectContaining( { + reporters: expect.arrayContaining( [ 'coverage' ] ) + } ) ); + + expect( loaders ).not.toEqual( expect.arrayContaining( [ 'babel-loader' ] ) ); + expect( loaders ).toEqual( expect.arrayContaining( [ 'other-loader' ] ) ); } ); - it( 'should return custom launchers with flags', () => { + it( 'should return custom browser launchers with flags', () => { const options = { reporter: 'mocha', + files: [ '*' ], globPatterns: { '*': 'workspace/packages/ckeditor5-*/tests/**/*.js' } @@ -193,30 +221,36 @@ describe( 'getKarmaConfig()', () => { const karmaConfig = getKarmaConfig( options ); - expect( karmaConfig ).to.have.own.property( 'customLaunchers' ); - expect( karmaConfig.customLaunchers ).to.have.own.property( 'CHROME_CI' ); - expect( karmaConfig.customLaunchers ).to.have.own.property( 'CHROME_LOCAL' ); - - expect( karmaConfig.customLaunchers.CHROME_CI ).to.have.own.property( 'base', 'Chrome' ); - expect( karmaConfig.customLaunchers.CHROME_CI ).to.have.own.property( 'flags' ); - expect( karmaConfig.customLaunchers.CHROME_CI.flags ).to.deep.equal( [ - '--disable-background-timer-throttling', - '--js-flags="--expose-gc"', - '--disable-renderer-backgrounding', - '--disable-backgrounding-occluded-windows', - '--disable-search-engine-choice-screen', - '--no-sandbox' - ] ); - - expect( karmaConfig.customLaunchers.CHROME_LOCAL ).to.have.own.property( 'base', 'Chrome' ); - expect( karmaConfig.customLaunchers.CHROME_LOCAL ).to.have.own.property( 'flags' ); - expect( karmaConfig.customLaunchers.CHROME_LOCAL.flags ).to.deep.equal( [ - '--disable-background-timer-throttling', - '--js-flags="--expose-gc"', - '--disable-renderer-backgrounding', - '--disable-backgrounding-occluded-windows', - '--disable-search-engine-choice-screen', - '--remote-debugging-port=9222' - ] ); + expect( karmaConfig ).toEqual( expect.objectContaining( { + customLaunchers: expect.any( Object ) + } ) ); + + expect( karmaConfig.customLaunchers ).toEqual( expect.objectContaining( { + CHROME_CI: expect.objectContaining( { + base: 'Chrome', + flags: [ + '--disable-background-timer-throttling', + '--js-flags="--expose-gc"', + '--disable-renderer-backgrounding', + '--disable-backgrounding-occluded-windows', + '--disable-search-engine-choice-screen', + '--no-sandbox' + ] + } ) + } ) ); + + expect( karmaConfig.customLaunchers ).toEqual( expect.objectContaining( { + CHROME_LOCAL: expect.objectContaining( { + base: 'Chrome', + flags: [ + '--disable-background-timer-throttling', + '--js-flags="--expose-gc"', + '--disable-renderer-backgrounding', + '--disable-backgrounding-occluded-windows', + '--disable-search-engine-choice-screen', + '--remote-debugging-port=9222' + ] + } ) + } ) ); } ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/getwebpackconfig.js b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/getwebpackconfig.js index c4831526e..3838ece6f 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/getwebpackconfig.js +++ b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/getwebpackconfig.js @@ -3,54 +3,22 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const mockery = require( 'mockery' ); -const sinon = require( 'sinon' ); -const { expect } = require( 'chai' ); +import { describe, expect, it, vi } from 'vitest'; +import { loaders } from '@ckeditor/ckeditor5-dev-utils'; +import TreatWarningsAsErrorsWebpackPlugin from '../../../lib/utils/automated-tests/treatwarningsaserrorswebpackplugin.js'; +import getWebpackConfigForAutomatedTests from '../../../lib/utils/automated-tests/getwebpackconfig.js'; +import getDefinitionsFromFile from '../../../lib/utils/getdefinitionsfromfile.js'; + +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( '../../../lib/utils/getdefinitionsfromfile.js' ); +vi.mock( '../../../lib/utils/automated-tests/treatwarningsaserrorswebpackplugin', () => ( { + default: class TreatWarningsAsErrorsWebpackPlugin {} +} ) ); describe( 'getWebpackConfigForAutomatedTests()', () => { - let getWebpackConfigForAutomatedTests, stubs; - - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - getDefinitionsFromFile: sinon.stub().returns( {} ), - loaders: { - getIconsLoader: sinon.stub().returns( {} ), - getStylesLoader: sinon.stub().returns( {} ), - getTypeScriptLoader: sinon.stub().returns( {} ), - getFormattedTextLoader: sinon.stub().returns( {} ), - getCoverageLoader: sinon.stub().returns( {} ), - getJavaScriptLoader: sinon.stub().returns( {} ) - }, - TreatWarningsAsErrorsWebpackPlugin: class TreatWarningsAsErrorsWebpackPlugin {} - }; - - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { loaders: stubs.loaders } ); - - mockery.registerMock( '../getdefinitionsfromfile', stubs.getDefinitionsFromFile ); - - mockery.registerMock( './treatwarningsaserrorswebpackplugin', stubs.TreatWarningsAsErrorsWebpackPlugin ); - - getWebpackConfigForAutomatedTests = require( '../../../lib/utils/automated-tests/getwebpackconfig' ); - } ); - - afterEach( () => { - sinon.restore(); - mockery.disable(); - mockery.deregisterAll(); - } ); - it( 'should return basic webpack configuration object', () => { - const debug = []; const webpackConfig = getWebpackConfigForAutomatedTests( { - debug, + debug: [], themePath: '/theme/path', tsconfig: '/tsconfig/path' } ); @@ -58,18 +26,16 @@ describe( 'getWebpackConfigForAutomatedTests()', () => { expect( webpackConfig.resolve.extensions ).to.deep.equal( [ '.ts', '.js', '.json' ] ); expect( webpackConfig.resolve.fallback.timers ).to.equal( false ); - expect( stubs.loaders.getIconsLoader.calledOnce ).to.equal( true ); - - expect( stubs.loaders.getStylesLoader.calledOnce ).to.equal( true ); - expect( stubs.loaders.getStylesLoader.firstCall.args[ 0 ] ).to.have.property( 'themePath', '/theme/path' ); - expect( stubs.loaders.getStylesLoader.firstCall.args[ 0 ] ).to.have.property( 'minify', true ); - - expect( stubs.loaders.getTypeScriptLoader.calledOnce ).to.equal( true ); - expect( stubs.loaders.getTypeScriptLoader.firstCall.args[ 0 ] ).to.have.property( 'configFile', '/tsconfig/path' ); - - expect( stubs.loaders.getFormattedTextLoader.calledOnce ).to.equal( true ); - - expect( stubs.loaders.getCoverageLoader.called ).to.equal( false ); + expect( vi.mocked( loaders.getIconsLoader ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( loaders.getFormattedTextLoader ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( loaders.getCoverageLoader ) ).not.toHaveBeenCalledOnce(); + expect( vi.mocked( loaders.getStylesLoader ) ).toHaveBeenCalledExactlyOnceWith( { + themePath: '/theme/path', + minify: true + } ); + expect( vi.mocked( loaders.getTypeScriptLoader ) ).toHaveBeenCalledExactlyOnceWith( { + configFile: '/tsconfig/path' + } ); expect( webpackConfig.resolveLoader.modules[ 0 ] ).to.equal( 'node_modules' ); expect( webpackConfig.devtool ).to.equal( undefined ); @@ -87,7 +53,8 @@ describe( 'getWebpackConfigForAutomatedTests()', () => { getWebpackConfigForAutomatedTests( { files: [ '**/*.js' ] } ); - expect( stubs.loaders.getJavaScriptLoader.called ).to.equal( false ); + + expect( vi.mocked( loaders.getJavaScriptLoader ) ).not.toHaveBeenCalledOnce(); } ); it( 'should return webpack configuration containing a loader for measuring the coverage', () => { @@ -96,7 +63,7 @@ describe( 'getWebpackConfigForAutomatedTests()', () => { files: [ '**/*.js' ] } ); - expect( stubs.loaders.getCoverageLoader.called ).to.equal( true ); + expect( vi.mocked( loaders.getCoverageLoader ) ).toHaveBeenCalledOnce(); } ); it( 'should return webpack configuration with source map support', () => { @@ -124,7 +91,7 @@ describe( 'getWebpackConfigForAutomatedTests()', () => { } ); it( 'should return webpack configuration with loaded identity file', () => { - stubs.getDefinitionsFromFile.returns( { LICENSE_KEY: 'secret' } ); + vi.mocked( getDefinitionsFromFile ).mockReturnValue( { LICENSE_KEY: 'secret' } ); const webpackConfig = getWebpackConfigForAutomatedTests( { identityFile: 'path/to/secrets.js' @@ -132,7 +99,7 @@ describe( 'getWebpackConfigForAutomatedTests()', () => { const plugin = webpackConfig.plugins[ 0 ]; - expect( stubs.getDefinitionsFromFile.firstCall.args[ 0 ] ).to.equal( 'path/to/secrets.js' ); + expect( vi.mocked( getDefinitionsFromFile ) ).toHaveBeenCalledExactlyOnceWith( 'path/to/secrets.js' ); expect( plugin.definitions.LICENSE_KEY ).to.equal( 'secret' ); } ); @@ -169,8 +136,9 @@ describe( 'getWebpackConfigForAutomatedTests()', () => { production: true } ); - expect( webpackConfig.plugins.filter( plugin => plugin instanceof stubs.TreatWarningsAsErrorsWebpackPlugin ) ) - .to.have.lengthOf( 1 ); + const plugin = webpackConfig.plugins.find( plugin => plugin instanceof TreatWarningsAsErrorsWebpackPlugin ); + + expect( plugin ).toBeTruthy(); } ); it( 'should load TypeScript files first when importing JS files', () => { diff --git a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/parsearguments.js b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/parsearguments.js index d76d9eb57..6c8937632 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/parsearguments.js +++ b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/parsearguments.js @@ -3,74 +3,35 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs' ); -const path = require( 'path' ); -const mockery = require( 'mockery' ); -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); - -const originalPosixJoin = path.posix.join; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'fs-extra'; +import { tools, logger } from '@ckeditor/ckeditor5-dev-utils'; +import parseArguments from '../../../lib/utils/automated-tests/parsearguments.js'; + +vi.mock( 'path', () => ( { + default: { + join: vi.fn( ( ...chunks ) => chunks.join( '/' ) ), + dirname: vi.fn() + } +} ) ); +vi.mock( 'fs-extra' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); describe( 'parseArguments()', () => { - let parseArguments, sandbox, stubs, packageName; + let logWarningStub; beforeEach( () => { - sandbox = sinon.createSandbox(); - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - cwd: sandbox.stub( process, 'cwd' ).callsFake( () => '/' ), - existsSync: sandbox.stub( fs, 'existsSync' ), - tools: { - getDirectories: sandbox.stub() - }, - logger: { - warning: sandbox.stub() - }, - fs: { - statSync: sandbox.stub() - }, - // To force unix paths in tests. - pathJoin: sandbox.stub( path, 'join' ).callsFake( ( ...chunks ) => originalPosixJoin( ...chunks ) ) - }; - - stubs.cwd.returns( '/home/project' ); - - packageName = 'ckeditor5'; - - mockery.registerMock( '/home/project/package.json', { - get name() { - return packageName; - } - } ); + logWarningStub = vi.fn(); - // mockery.registerMock( 'fs', stubs.fs ); - - parseArguments = proxyquire( '../../../lib/utils/automated-tests/parsearguments', { - '@ckeditor/ckeditor5-dev-utils': { - logger() { - return stubs.logger; - }, - tools: stubs.tools - }, - 'fs': stubs.fs + vi.spyOn( process, 'cwd' ).mockReturnValue( '/home/project' ); + vi.mocked( logger ).mockReturnValue( { + warning: logWarningStub } ); } ); - afterEach( () => { - sandbox.restore(); - mockery.disable(); - } ); - it( 'replaces kebab-case strings with camelCase values', () => { + vi.mocked( fs ).readJsonSync.mockReturnValue( {} ); + const options = parseArguments( [ '--source-map', 'true', @@ -97,6 +58,8 @@ describe( 'parseArguments()', () => { } ); it( 'deletes all aliases keys from returned object', () => { + vi.mocked( fs ).readJsonSync.mockReturnValue( {} ); + const options = parseArguments( [ '-b', 'Chrome,Firefox', @@ -190,63 +153,67 @@ describe( 'parseArguments()', () => { expect( options.repositories ).to.deep.equal( [] ); } ); - it( - 'returns an array of packages to tests when `--repositories` is specified ' + - '(root directory check)', - () => { - packageName = 'ckeditor5'; - - stubs.fs.statSync.withArgs( '/home/project/external' ).throws( 'ENOENT: no such file or directory' ); - - stubs.tools.getDirectories.withArgs( '/home/project/packages' ).returns( [ - 'ckeditor5-core', - 'ckeditor5-engine' - ] ); + it( 'returns an array of packages to tests when `--repositories` is specified (root directory check)', () => { + vi.mocked( fs ).readJsonSync.mockReturnValue( { name: 'ckeditor5' } ); + vi.mocked( fs ).statSync.mockImplementation( input => { + if ( input === '/home/project/external' ) { + throw new Error( 'ENOENT: no such file or directory' ); + } + } ); + vi.mocked( tools ).getDirectories.mockImplementation( input => { + if ( input === '/home/project/packages' ) { + return [ + 'ckeditor5-core', + 'ckeditor5-engine' + ]; + } + } ); - const options = parseArguments( [ - '--repositories', - 'ckeditor5' - ] ); - - expect( options.files ).to.deep.equal( [ 'core', 'engine' ] ); + const options = parseArguments( [ + '--repositories', + 'ckeditor5' + ] ); - expect( stubs.logger.warning.callCount ).to.equal( 1 ); - expect( stubs.logger.warning.firstCall.args[ 0 ] ).to.equal( - 'The `external/` directory does not exist. Only the root repository will be checked.' - ); - } - ); + expect( options.files ).to.deep.equal( [ 'core', 'engine' ] ); - it( - 'returns an array of packages to tests when `--repositories` is specified ' + - '(external directory check)', - () => { - packageName = 'foo'; + expect( logWarningStub ).toHaveBeenCalledExactlyOnceWith( + 'The `external/` directory does not exist. Only the root repository will be checked.' + ); + } ); - stubs.fs.statSync.returns( { isDirectory: () => true } ); - stubs.tools.getDirectories.withArgs( '/home/project/external/ckeditor5/packages' ).returns( [ - 'ckeditor5-core', - 'ckeditor5-engine' - ] ); + it( 'returns an array of packages to tests when `--repositories` is specified (external directory check)', () => { + vi.mocked( fs ).readJsonSync.mockReturnValue( { name: 'foo' } ); + vi.mocked( fs ).statSync.mockReturnValue( { isDirectory: () => true } ); + vi.mocked( tools ).getDirectories.mockImplementation( input => { + if ( input === '/home/project/external/ckeditor5/packages' ) { + return [ + 'ckeditor5-core', + 'ckeditor5-engine' + ]; + } + } ); - const options = parseArguments( [ - '--repositories', - 'ckeditor5' - ] ); + const options = parseArguments( [ + '--repositories', + 'ckeditor5' + ] ); - expect( options.files ).to.deep.equal( [ 'core', 'engine' ] ); - expect( stubs.logger.warning.callCount ).to.equal( 0 ); - } - ); + expect( options.files ).to.deep.equal( [ 'core', 'engine' ] ); + expect( logWarningStub ).not.toHaveBeenCalled(); + } ); it( 'returns an array of packages to tests when `--repositories` is specified ' + '(external directory check, specified repository does not exist)', () => { - packageName = 'foo'; + vi.mocked( fs ).readJsonSync.mockReturnValue( { name: 'foo' } ); + vi.mocked( fs ).statSync.mockImplementation( input => { + if ( input === '/home/project/external' ) { + return { isDirectory: () => true }; + } - stubs.fs.statSync.withArgs( '/home/project/external' ).returns( { isDirectory: () => true } ); - stubs.fs.statSync.withArgs( '/home/project/external/ckeditor5' ).throws( 'ENOENT: no such file or directory' ); + throw new Error( 'ENOENT: no such file or directory' ); + } ); const options = parseArguments( [ '--repositories', @@ -254,9 +221,7 @@ describe( 'parseArguments()', () => { ] ); expect( options.files ).to.deep.equal( [] ); - - expect( stubs.logger.warning.callCount ).to.equal( 1 ); - expect( stubs.logger.warning.firstCall.args[ 0 ] ).to.equal( + expect( logWarningStub ).toHaveBeenCalledExactlyOnceWith( 'Did not find the repository "ckeditor5" in the root repository or the "external/" directory.' ); } @@ -266,14 +231,23 @@ describe( 'parseArguments()', () => { 'returns an array of packages (unique list) to tests when `--repositories` is specified ' + '(root directory check + `--files` specified)', () => { - packageName = 'ckeditor5'; - - stubs.fs.statSync.withArgs( '/home/project/external' ).returns( { isDirectory: () => true } ); - stubs.tools.getDirectories.withArgs( '/home/project/packages' ).returns( [ - 'ckeditor5-core', - 'ckeditor5-engine', - 'ckeditor5-utils' - ] ); + vi.mocked( fs ).readJsonSync.mockReturnValue( { name: 'ckeditor5' } ); + vi.mocked( fs ).statSync.mockImplementation( input => { + if ( input === '/home/project/external' ) { + return { isDirectory: () => true }; + } + + throw new Error( 'ENOENT: no such file or directory' ); + } ); + vi.mocked( tools ).getDirectories.mockImplementation( input => { + if ( input === '/home/project/packages' ) { + return [ + 'ckeditor5-core', + 'ckeditor5-engine', + 'ckeditor5-utils' + ]; + } + } ); const options = parseArguments( [ '--repositories', @@ -290,23 +264,28 @@ describe( 'parseArguments()', () => { 'returns an array of packages to tests when `--repositories` is specified ' + '(root and external directories check)', () => { - packageName = 'ckeditor5'; - - stubs.fs.statSync.withArgs( '/home/project/external' ).returns( { isDirectory: () => true } ); - stubs.fs.statSync.withArgs( '/home/project/external/foo' ).returns( { isDirectory: () => true } ); - stubs.fs.statSync.withArgs( '/home/project/external/bar' ).returns( { isDirectory: () => true } ); - stubs.tools.getDirectories.withArgs( '/home/project/packages' ).returns( [ - 'ckeditor5-core', - 'ckeditor5-engine' - ] ); - stubs.tools.getDirectories.withArgs( '/home/project/external/foo/packages' ).returns( [ - 'ckeditor5-foo-1', - 'ckeditor5-foo-2' - ] ); - stubs.tools.getDirectories.withArgs( '/home/project/external/bar/packages' ).returns( [ - 'ckeditor5-bar-1', - 'ckeditor5-bar-2' - ] ); + vi.mocked( fs ).readJsonSync.mockReturnValue( { name: 'ckeditor5' } ); + vi.mocked( fs ).statSync.mockReturnValue( { isDirectory: () => true } ); + vi.mocked( tools ).getDirectories.mockImplementation( input => { + if ( input === '/home/project/packages' ) { + return [ + 'ckeditor5-core', + 'ckeditor5-engine' + ]; + } + if ( input === '/home/project/external/foo/packages' ) { + return [ + 'ckeditor5-foo-1', + 'ckeditor5-foo-2' + ]; + } + if ( input === '/home/project/external/bar/packages' ) { + return [ + 'ckeditor5-bar-1', + 'ckeditor5-bar-2' + ]; + } + } ); const options = parseArguments( [ '--repositories', @@ -340,7 +319,7 @@ describe( 'parseArguments()', () => { describe( 'tsconfig', () => { it( 'should be null by default, if `tsconfig.test.json` does not exist', () => { - stubs.existsSync.returns( false ); + vi.mocked( fs ).existsSync.mockReturnValue( false ); const options = parseArguments( [] ); @@ -348,8 +327,7 @@ describe( 'parseArguments()', () => { } ); it( 'should use `tsconfig.test.json` from `cwd` if it is available by default', () => { - stubs.cwd.returns( '/home/project' ); - stubs.existsSync.returns( true ); + vi.mocked( fs ).existsSync.mockReturnValue( true ); const options = parseArguments( [] ); @@ -357,16 +335,16 @@ describe( 'parseArguments()', () => { } ); it( 'should parse `--tsconfig` to absolute path if it is set and it exists', () => { - stubs.cwd.returns( '/home/project' ); - stubs.existsSync.returns( true ); - const options = parseArguments( [ '--tsconfig', './configs/tsconfig.json' ] ); + vi.mocked( fs ).existsSync.mockReturnValue( true ); + + const options = parseArguments( [ '--tsconfig', 'configs/tsconfig.json' ] ); expect( options.tsconfig ).to.be.equal( '/home/project/configs/tsconfig.json' ); } ); it( 'should be null if `--tsconfig` points to non-existing file', () => { - stubs.cwd.returns( '/home/project' ); - stubs.existsSync.returns( false ); + vi.mocked( fs ).existsSync.mockReturnValue( false ); + const options = parseArguments( [ '--tsconfig', './configs/tsconfig.json' ] ); expect( options.tsconfig ).to.equal( null ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/treatwarningsaserrorswebpackplugin.js b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/treatwarningsaserrorswebpackplugin.js index 326cb611c..9b323263c 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/automated-tests/treatwarningsaserrorswebpackplugin.js +++ b/packages/ckeditor5-dev-tests/tests/utils/automated-tests/treatwarningsaserrorswebpackplugin.js @@ -3,59 +3,68 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it } from 'vitest'; +import webpack from 'webpack'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import TreatWarningsAsErrorsWebpackPlugin from '../../../lib/utils/automated-tests/treatwarningsaserrorswebpackplugin.js'; -const webpack = require( 'webpack' ); -const path = require( 'path' ); -const { expect } = require( 'chai' ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); -describe( 'TreatWarningsAsErrorsWebpackPlugin()', () => { - let TreatWarningsAsErrorsWebpackPlugin; +describe( 'TreatWarningsAsErrorsWebpackPlugin', () => { + it( 'should reassign warnings to errors and not emit the code when errors are present', () => { + return new Promise( ( resolve, reject ) => { + runCompiler( + { + mode: 'development', + entry: './treatwarningsaserrorswebpackplugin/entrypoint.cjs', + plugins: [ + { + apply( compiler ) { + compiler.hooks.make.tap( 'MakeCompilationWarning', compilation => { + compilation.errors.push( new Error( 'Compilation error 1' ) ); + compilation.errors.push( new Error( 'Compilation error 2' ) ); + compilation.warnings.push( new Error( 'Compilation warning 1' ) ); + compilation.warnings.push( new Error( 'Compilation warning 2' ) ); + } ); + } + }, + new TreatWarningsAsErrorsWebpackPlugin() + ] + }, + ( err, stats ) => { + if ( err ) { + return reject( err ); + } - beforeEach( () => { - TreatWarningsAsErrorsWebpackPlugin = require( '../../../lib/utils/automated-tests/treatwarningsaserrorswebpackplugin' ); - } ); + try { + const statsJson = stats.toJson( { errorDetails: false } ); - it( 'should reassign warnings to errors and not emit the code when errors are present', done => { - runCompiler( { - mode: 'development', - entry: './file', - plugins: [ - { - apply( compiler ) { - compiler.hooks.make.tap( 'MakeCompilationWarning', compilation => { - compilation.errors.push( new Error( 'Compilation error 1' ) ); - compilation.errors.push( new Error( 'Compilation error 2' ) ); - compilation.warnings.push( new Error( 'Compilation warning 1' ) ); - compilation.warnings.push( new Error( 'Compilation warning 2' ) ); - } ); + expect( statsJson.errors.length ).to.equal( 4 ); + expect( statsJson.warnings.length ).to.equal( 0 ); + expect( statsJson.errors[ 0 ].message ).to.equal( 'Compilation error 1' ); + expect( statsJson.errors[ 1 ].message ).to.equal( 'Compilation error 2' ); + expect( statsJson.errors[ 2 ].message ).to.equal( 'Compilation warning 1' ); + expect( statsJson.errors[ 3 ].message ).to.equal( 'Compilation warning 2' ); + expect( statsJson.assets[ 0 ].emitted ).to.equal( false ); + resolve(); + } catch ( error ) { + reject( error ); } - }, - new TreatWarningsAsErrorsWebpackPlugin() - ] - }, stats => { - const statsJson = stats.toJson( { errorDetails: false } ); - - expect( statsJson.errors.length ).to.equal( 4 ); - expect( statsJson.warnings.length ).to.equal( 0 ); - expect( statsJson.errors[ 0 ].message ).to.equal( 'Compilation error 1' ); - expect( statsJson.errors[ 1 ].message ).to.equal( 'Compilation error 2' ); - expect( statsJson.errors[ 2 ].message ).to.equal( 'Compilation warning 1' ); - expect( statsJson.errors[ 3 ].message ).to.equal( 'Compilation warning 2' ); - expect( statsJson.assets[ 0 ].emitted ).to.equal( false ); - done(); + } ); } ); } ); } ); function runCompiler( options, callback ) { - options.context = path.join( __dirname, 'fixtures' ); + options.context = path.join( __dirname, '..', '..', 'fixtures' ); const compiler = webpack( options ); compiler.outputFileSystem = {}; compiler.run( ( err, stats ) => { - callback( stats ); + callback( err, stats ); } ); } diff --git a/packages/ckeditor5-dev-tests/tests/utils/getdefinitionsfromfile.js b/packages/ckeditor5-dev-tests/tests/utils/getdefinitionsfromfile.js index 2525b1846..28e88923b 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/getdefinitionsfromfile.js +++ b/packages/ckeditor5-dev-tests/tests/utils/getdefinitionsfromfile.js @@ -3,49 +3,20 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const mockery = require( 'mockery' ); -const sinon = require( 'sinon' ); -const { expect } = require( 'chai' ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import path from 'path'; +import getDefinitionsFromFile from '../../lib/utils/getdefinitionsfromfile.js'; describe( 'getDefinitionsFromFile()', () => { - let getDefinitionsFromFile, consoleStub; - beforeEach( () => { - consoleStub = sinon.stub( console, 'error' ); - sinon.stub( process, 'cwd' ).returns( '/workspace' ); - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'path', { - join: sinon.stub().callsFake( ( ...chunks ) => chunks.join( '/' ).replace( '/./', '/' ) ), - isAbsolute: sinon.stub().callsFake( path => path.startsWith( '/' ) ) - } ); - - mockery.registerMock( '/workspace/path/to/secret.js', { - SECRET: 'secret', - ANOTHER_SECRET: 'another-secret', - NON_PRIMITIVE_SECRET: { - foo: [ 'bar', 'baz' ] - } - } ); - - getDefinitionsFromFile = require( '../../lib/utils/getdefinitionsfromfile' ); - } ); - - afterEach( () => { - sinon.restore(); - mockery.disable(); - mockery.deregisterAll(); + vi.spyOn( path, 'join' ).mockImplementation( ( ...chunks ) => chunks.join( '/' ).replace( '/./', '/' ) ); } ); it( 'should return definition object if path to identity file is relative', () => { - const definitions = getDefinitionsFromFile( './path/to/secret.js' ); + const definitions = getDefinitionsFromFile( + // A relative path according to a package root. + path.join( '.', 'tests', 'fixtures', 'getdefinitionsfromfile', 'secret.cjs' ) + ); expect( definitions ).to.deep.equal( { SECRET: '"secret"', @@ -55,7 +26,9 @@ describe( 'getDefinitionsFromFile()', () => { } ); it( 'should return definition object if path to identity file is absolute', () => { - const definitions = getDefinitionsFromFile( '/workspace/path/to/secret.js' ); + const definitions = getDefinitionsFromFile( + path.join( __dirname, '..', 'fixtures', 'getdefinitionsfromfile', 'secret.cjs' ) + ); expect( definitions ).to.deep.equal( { SECRET: '"secret"', @@ -71,28 +44,32 @@ describe( 'getDefinitionsFromFile()', () => { } ); it( 'should not throw an error and return empty object if path to identity file is not valid', () => { + const consoleStub = vi.spyOn( console, 'error' ).mockImplementation( () => {} ); let definitions; expect( () => { definitions = getDefinitionsFromFile( 'foo.js' ); } ).to.not.throw(); - expect( consoleStub.callCount ).to.equal( 1 ); - expect( consoleStub.firstCall.args[ 0 ] ).to.satisfy( msg => msg.startsWith( 'Cannot find module \'/workspace/foo.js\'' ) ); + expect( consoleStub ).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining( 'Cannot find module' ) + ); expect( definitions ).to.deep.equal( {} ); } ); - it( 'should not throw an error and return empty object if stringifying the identity file has failed', () => { - sinon.stub( JSON, 'stringify' ).throws( new Error( 'Example error.' ) ); + it( 'should not throw an error and return empty object if stringifies the identity file has failed', () => { + const consoleStub = vi.spyOn( console, 'error' ).mockImplementation( () => {} ); + vi.spyOn( JSON, 'stringify' ).mockImplementation( () => { + throw new Error( 'Example error.' ); + } ); let definitions; expect( () => { - definitions = getDefinitionsFromFile( '/workspace/path/to/secret.js' ); + definitions = getDefinitionsFromFile( path.join( '.', 'tests', 'fixtures', 'getdefinitionsfromfile', 'secret.cjs' ) ); } ).to.not.throw(); - expect( consoleStub.callCount ).to.equal( 1 ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( 'Example error.' ); + expect( consoleStub ).toHaveBeenCalledExactlyOnceWith( 'Example error.' ); expect( definitions ).to.deep.equal( {} ); } ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/getrelativefilepath.js b/packages/ckeditor5-dev-tests/tests/utils/getrelativefilepath.js index d1d763179..8e110a742 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/getrelativefilepath.js +++ b/packages/ckeditor5-dev-tests/tests/utils/getrelativefilepath.js @@ -3,142 +3,122 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const sinon = require( 'sinon' ); -const { expect } = require( 'chai' ); - -describe( 'dev-tests/utils', () => { - let getRelativeFilePath; - - beforeEach( () => { - getRelativeFilePath = require( '../../lib/utils/getrelativefilepath' ); - } ); - - describe( 'getRelativeFilePath()', () => { - let sandbox; +import path from 'path'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import getRelativeFilePath from '../../lib/utils/getrelativefilepath.js'; +describe( 'getRelativeFilePath()', () => { + describe( 'Unix paths', () => { beforeEach( () => { - sandbox = sinon.createSandbox(); - } ); - - afterEach( () => { - sandbox.restore(); + vi.spyOn( path, 'join' ).mockImplementation( ( ...args ) => args.join( '/' ) ); } ); - describe( 'Unix paths', () => { - beforeEach( () => { - sandbox.stub( path, 'join' ).callsFake( ( ...args ) => args.join( '/' ) ); - } ); + it( 'returns path which starts with package name (simple check)', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/Users/foo' ); - it( 'returns path which starts with package name (simple check)', () => { - sandbox.stub( process, 'cwd' ).returns( '/Users/foo' ); + checkPath( '/Users/foo/packages/ckeditor5-foo/tests/manual/foo.js', 'ckeditor5-foo/tests/manual/foo.js' ); + } ); - checkPath( '/Users/foo/packages/ckeditor5-foo/tests/manual/foo.js', 'ckeditor5-foo/tests/manual/foo.js' ); - } ); + it( 'returns path which starts with package name (workspace directory looks like package name)', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/Users/foo/ckeditor5-workspace/ckeditor5' ); - it( 'returns path which starts with package name (workspace directory looks like package name)', () => { - sandbox.stub( process, 'cwd' ).returns( '/Users/foo/ckeditor5-workspace/ckeditor5' ); + checkPath( + '/Users/foo/ckeditor5-workspace/ckeditor5/packages/ckeditor5-foo/tests/manual/foo.js', + 'ckeditor5-foo/tests/manual/foo.js' + ); + } ); - checkPath( - '/Users/foo/ckeditor5-workspace/ckeditor5/packages/ckeditor5-foo/tests/manual/foo.js', - 'ckeditor5-foo/tests/manual/foo.js' - ); - } ); + it( 'returns a proper path for "ckeditor-" prefix', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/work/space' ); - it( 'returns a proper path for "ckeditor-" prefix', () => { - sandbox.stub( process, 'cwd' ).returns( '/work/space' ); + checkPath( '/work/space/packages/ckeditor-foo/tests/manual/foo.js', 'ckeditor-foo/tests/manual/foo.js' ); + } ); - checkPath( '/work/space/packages/ckeditor-foo/tests/manual/foo.js', 'ckeditor-foo/tests/manual/foo.js' ); - } ); + it( 'returns a proper path for "ckeditor-" prefix and "ckeditor.js" file', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/work/space' ); - it( 'returns a proper path for "ckeditor-" prefix and "ckeditor.js" file', () => { - sandbox.stub( process, 'cwd' ).returns( '/work/space' ); + checkPath( '/work/space/packages/ckeditor-foo/tests/manual/ckeditor.js', 'ckeditor-foo/tests/manual/ckeditor.js' ); + } ); - checkPath( '/work/space/packages/ckeditor-foo/tests/manual/ckeditor.js', 'ckeditor-foo/tests/manual/ckeditor.js' ); - } ); + it( 'returns a proper path to from the main (root) package', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/work/space' ); + checkPath( '/work/space/packages/ckeditor5/tests/manual/foo.js', 'ckeditor5/tests/manual/foo.js' ); + } ); - it( 'returns a proper path to from the main (root) package', () => { - sandbox.stub( process, 'cwd' ).returns( '/work/space' ); - checkPath( '/work/space/packages/ckeditor5/tests/manual/foo.js', 'ckeditor5/tests/manual/foo.js' ); - } ); + it( 'returns a proper path for "ckeditor5.js" file', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/work/space' ); + checkPath( + '/work/space/packages/ckeditor5-build-a/tests/manual/ckeditor5.js', + 'ckeditor5-build-a/tests/manual/ckeditor5.js' + ); + } ); - it( 'returns a proper path for "ckeditor5.js" file', () => { - sandbox.stub( process, 'cwd' ).returns( '/work/space' ); - checkPath( - '/work/space/packages/ckeditor5-build-a/tests/manual/ckeditor5.js', - 'ckeditor5-build-a/tests/manual/ckeditor5.js' - ); - } ); + it( 'returns a proper path for "ckeditor.js" file', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( '/work/space' ); + checkPath( + '/work/space/packages/ckeditor5-build-a/tests/manual/ckeditor.js', + 'ckeditor5-build-a/tests/manual/ckeditor.js' ); + } ); + } ); - it( 'returns a proper path for "ckeditor.js" file', () => { - sandbox.stub( process, 'cwd' ).returns( '/work/space' ); - checkPath( - '/work/space/packages/ckeditor5-build-a/tests/manual/ckeditor.js', - 'ckeditor5-build-a/tests/manual/ckeditor.js' ); - } ); + describe( 'Windows paths', () => { + beforeEach( () => { + vi.spyOn( path, 'join' ).mockImplementation( ( ...args ) => args.join( '\\' ) ); } ); - describe( 'Windows paths', () => { - beforeEach( () => { - sandbox.stub( path, 'join' ).callsFake( ( ...args ) => args.join( '\\' ) ); - } ); + it( 'returns path which starts with package name (simple check)', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( 'C:\\work\\space' ); - it( 'returns path which starts with package name (simple check)', () => { - sandbox.stub( process, 'cwd' ).returns( 'C:\\work\\space' ); + checkPath( 'C:\\work\\space\\packages\\ckeditor5-foo\\tests\\manual\\foo.js', 'ckeditor5-foo\\tests\\manual\\foo.js' ); + } ); - checkPath( 'C:\\work\\space\\packages\\ckeditor5-foo\\tests\\manual\\foo.js', 'ckeditor5-foo\\tests\\manual\\foo.js' ); - } ); + it( 'returns path which starts with package name (workspace directory looks like package name)', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( 'C:\\Document and settings\\foo\\ckeditor5-workspace\\ckeditor5' ); - it( 'returns path which starts with package name (workspace directory looks like package name)', () => { - sandbox.stub( process, 'cwd' ).returns( 'C:\\Document and settings\\foo\\ckeditor5-workspace\\ckeditor5' ); + checkPath( + 'C:\\Document and settings\\foo\\ckeditor5-workspace\\ckeditor5\\packages\\ckeditor5-foo\\tests\\manual\\foo.js', + 'ckeditor5-foo\\tests\\manual\\foo.js' + ); + } ); - checkPath( - 'C:\\Document and settings\\foo\\ckeditor5-workspace\\ckeditor5\\packages\\ckeditor5-foo\\tests\\manual\\foo.js', - 'ckeditor5-foo\\tests\\manual\\foo.js' - ); - } ); + it( 'returns a proper path for "ckeditor-" prefix', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( 'C:\\work\\space' ); - it( 'returns a proper path for "ckeditor-" prefix', () => { - sandbox.stub( process, 'cwd' ).returns( 'C:\\work\\space' ); + checkPath( 'C:\\work\\space\\packages\\ckeditor-foo\\tests\\manual\\foo.js', 'ckeditor-foo\\tests\\manual\\foo.js' ); + } ); - checkPath( 'C:\\work\\space\\packages\\ckeditor-foo\\tests\\manual\\foo.js', 'ckeditor-foo\\tests\\manual\\foo.js' ); - } ); + it( 'returns a proper path for "ckeditor-" prefix and "ckeditor.js" file', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( 'C:\\work\\space' ); - it( 'returns a proper path for "ckeditor-" prefix and "ckeditor.js" file', () => { - sandbox.stub( process, 'cwd' ).returns( 'C:\\work\\space' ); + checkPath( + 'C:\\work\\space\\packages\\ckeditor-foo\\tests\\manual\\ckeditor.js', + 'ckeditor-foo\\tests\\manual\\ckeditor.js' + ); + } ); - checkPath( - 'C:\\work\\space\\packages\\ckeditor-foo\\tests\\manual\\ckeditor.js', - 'ckeditor-foo\\tests\\manual\\ckeditor.js' - ); - } ); + it( 'returns a proper path to from the main (root) package', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( 'C:\\work\\space' ); + checkPath( 'C:\\work\\space\\tests\\manual\\foo.js', 'ckeditor5\\tests\\manual\\foo.js' ); + } ); - it( 'returns a proper path to from the main (root) package', () => { - sandbox.stub( process, 'cwd' ).returns( 'C:\\work\\space' ); - checkPath( 'C:\\work\\space\\tests\\manual\\foo.js', 'ckeditor5\\tests\\manual\\foo.js' ); - } ); + it( 'returns a proper path for "ckeditor5.js" file', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( 'C:\\work\\space' ); + checkPath( + 'C:\\work\\space\\packages\\ckeditor5-build-a\\tests\\manual\\ckeditor5.js', + 'ckeditor5-build-a\\tests\\manual\\ckeditor5.js' + ); + } ); - it( 'returns a proper path for "ckeditor5.js" file', () => { - sandbox.stub( process, 'cwd' ).returns( 'C:\\work\\space' ); - checkPath( - 'C:\\work\\space\\packages\\ckeditor5-build-a\\tests\\manual\\ckeditor5.js', - 'ckeditor5-build-a\\tests\\manual\\ckeditor5.js' - ); - } ); - - it( 'returns a proper path for "ckeditor.js" file', () => { - sandbox.stub( process, 'cwd' ).returns( 'C:\\work\\space' ); - checkPath( - 'C:\\work\\space\\packages\\ckeditor5-build-a\\tests\\manual\\ckeditor.js', - 'ckeditor5-build-a\\tests\\manual\\ckeditor.js' - ); - } ); + it( 'returns a proper path for "ckeditor.js" file', () => { + vi.spyOn( process, 'cwd' ).mockReturnValue( 'C:\\work\\space' ); + checkPath( + 'C:\\work\\space\\packages\\ckeditor5-build-a\\tests\\manual\\ckeditor.js', + 'ckeditor5-build-a\\tests\\manual\\ckeditor.js' + ); } ); } ); - - function checkPath( filePath, expectedPath ) { - expect( getRelativeFilePath( filePath ) ).to.equal( expectedPath ); - } } ); + +function checkPath( filePath, expectedPath ) { + expect( getRelativeFilePath( filePath ) ).to.equal( expectedPath ); +} diff --git a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/compilehtmlfiles.js b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/compilehtmlfiles.js index e94a529da..cc2931fb0 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/compilehtmlfiles.js +++ b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/compilehtmlfiles.js @@ -3,128 +3,109 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const mockery = require( 'mockery' ); -const sinon = require( 'sinon' ); -const { use, expect } = require( 'chai' ); -const chokidar = require( 'chokidar' ); -const sinonChai = require( 'sinon-chai' ); - -use( sinonChai ); - -const fakeDirname = path.dirname( require.resolve( '../../../lib/utils/manual-tests/compilehtmlfiles' ) ); - -describe( 'compileHtmlFiles', () => { - let sandbox, stubs, files, compileHtmlFiles; +import path from 'path'; +import fs from 'fs-extra'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import { globSync } from 'glob'; +import chokidar from 'chokidar'; +import chalk from 'chalk'; +import domCombiner from 'dom-combiner'; +import compileHtmlFiles from '../../../lib/utils/manual-tests/compilehtmlfiles.js'; +import getRelativeFilePath from '../../../lib/utils/getrelativefilepath.js'; + +const stubs = vi.hoisted( () => ( { + commonmark: { + parser: { + parse: vi.fn() + }, + htmlRenderer: { + render: vi.fn() + } + }, + log: { + info: vi.fn() + } +} ) ); + +vi.mock( 'path' ); +vi.mock( 'commonmark', () => ( { + Parser: class Parser { + parse( ...args ) { + return stubs.commonmark.parser.parse( ...args ); + } + }, + + HtmlRenderer: class HtmlRenderer { + render( ...args ) { + return stubs.commonmark.htmlRenderer.render( ...args ); + } + } +} ) ); +vi.mock( 'path' ); +vi.mock( 'glob' ); +vi.mock( 'fs-extra' ); +vi.mock( 'chokidar' ); +vi.mock( 'chalk', () => ( { + default: { + cyan: vi.fn() + } +} ) ); +vi.mock( 'dom-combiner' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( '../../../lib/utils/getrelativefilepath.js' ); + +describe( 'compileHtmlFiles()', () => { + let files; let patternFiles = {}; let separator = '/'; beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - sandbox = sinon.createSandbox(); - - stubs = { - fs: { - readFileSync: sandbox.spy( pathToFile => files[ pathToFile ] ), - ensureDirSync: sandbox.stub(), - outputFileSync: sandbox.stub(), - copySync: sandbox.stub() - }, - - path: { - join: sandbox.stub().callsFake( ( ...chunks ) => chunks.join( separator ) ), - parse: sandbox.stub().callsFake( pathToParse => { - const chunks = pathToParse.split( separator ); - const fileName = chunks.pop(); - - return { - dir: chunks.join( separator ), - name: fileName.split( '.' ).slice( 0, -1 ).join( '.' ) - }; - } ), - dirname: sandbox.stub().callsFake( pathToParse => { - return pathToParse.split( separator ).slice( 0, -1 ).join( separator ); - } ), - sep: separator - }, - - logger: { - info: sandbox.stub(), - warning: sandbox.stub(), - error: sandbox.stub() - }, - - commonmark: { - parse: sandbox.spy(), - render: sandbox.spy( () => '

Markdown header

' ) - }, - - chalk: { - cyan: sandbox.spy( text => text ) - }, - - chokidar: { - watch: sandbox.stub( chokidar, 'watch' ).callsFake( () => ( { - on: () => { - } - } ) ) - }, - - getRelativeFilePath: sandbox.spy( pathToFile => pathToFile ), - glob: { - globSync: sandbox.spy( pattern => patternFiles[ pattern ] ) - }, - domCombiner: sandbox.spy( ( ...args ) => args.join( '\n' ) ) - }; - - mockery.registerMock( 'path', stubs.path ); - mockery.registerMock( 'commonmark', { - Parser: class Parser { - parse( ...args ) { - return stubs.commonmark.parse( ...args ); - } - }, - - HtmlRenderer: class HtmlRenderer { - render( ...args ) { - return stubs.commonmark.render( ...args ); - } - } + stubs.commonmark.htmlRenderer.render.mockReturnValue( '

Markdown header

' ); + vi.mocked( logger ).mockReturnValue( stubs.log ); + vi.mocked( chalk ).cyan.mockImplementation( input => input ); + vi.mocked( fs ).readFileSync.mockImplementation( pathToFile => files[ pathToFile ] ); + vi.mocked( path ).join.mockImplementation( ( ...chunks ) => chunks.join( separator ) ); + vi.mocked( path ).parse.mockImplementation( pathToParse => { + const chunks = pathToParse.split( separator ); + const fileName = chunks.pop(); + + return { + dir: chunks.join( separator ), + name: fileName.split( '.' ).slice( 0, -1 ).join( '.' ) + }; } ); - mockery.registerMock( 'glob', stubs.glob ); - mockery.registerMock( 'fs-extra', stubs.fs ); - mockery.registerMock( 'chokidar', stubs.chokidar ); - mockery.registerMock( 'dom-combiner', stubs.domCombiner ); - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { - logger() { - return stubs.logger; - } + vi.mocked( path ).dirname.mockImplementation( pathToParse => { + return pathToParse.split( separator ).slice( 0, -1 ).join( separator ); } ); - mockery.registerMock( '../getrelativefilepath', stubs.getRelativeFilePath ); - } ); - - afterEach( () => { - sandbox.restore(); - mockery.deregisterAll(); - mockery.disable(); + vi.mocked( chokidar ).watch.mockImplementation( () => ( { + on: vi.fn() + } ) ); + vi.mocked( getRelativeFilePath ).mockImplementation( pathToFile => pathToFile ); + vi.mocked( globSync ).mockImplementation( pattern => patternFiles[ pattern ] ); + vi.mocked( domCombiner ).mockImplementation( ( ...args ) => args.join( '\n' ) ); } ); describe( 'Unix environment', () => { beforeEach( () => { separator = '/'; - compileHtmlFiles = require( '../../../lib/utils/manual-tests/compilehtmlfiles' ); + } ); + + it( 'creates a build directory where compiled files are saved', () => { + files = {}; + + compileHtmlFiles( { + buildDir: 'buildDir', + language: 'en', + sourceFiles: [] + } ); + + expect( vi.mocked( fs ).ensureDirSync ).toHaveBeenCalledExactlyOnceWith( 'buildDir' ); } ); it( 'should compile md and html files to the output html file', () => { files = { - [ `${ fakeDirname }/template.html` ]: '
template html content
', + '/template.html': '
template html content
', 'path/to/manual/file.md': '## Markdown header', 'path/to/manual/file.html': '
html file content
' }; @@ -139,50 +120,93 @@ describe( 'compileHtmlFiles', () => { sourceFiles: [ 'path/to/manual/file.js' ] } ); - expect( stubs.commonmark.parse ).to.be.calledWithExactly( '## Markdown header' ); - expect( stubs.fs.ensureDirSync ).to.be.calledWithExactly( 'buildDir' ); + expect( stubs.commonmark.parser.parse ).toHaveBeenCalledExactlyOnceWith( '## Markdown header' ); - /* eslint-disable max-len */ - expect( stubs.fs.outputFileSync ).to.be.calledWithExactly( + expect( vi.mocked( fs ).outputFileSync ).toHaveBeenCalledExactlyOnceWith( 'buildDir/path/to/manual/file.html', [ '
template html content
', '

Markdown header

', '', '' + - '' + + '' + '', '
html file content
', '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + '' ].join( '\n' ) ); - /* eslint-enable max-len */ - expect( stubs.chokidar.watch ).to.be.calledWithExactly( + expect( stubs.log.info ).toHaveBeenCalledTimes( 2 ); + expect( stubs.log.info ).toHaveBeenCalledWith( expect.stringMatching( /^Processing/ ) ); + expect( stubs.log.info ).toHaveBeenCalledWith( expect.stringMatching( /^Finished writing/ ) ); + } ); + + it( 'should listen to changes in source files', () => { + files = { + '/template.html': '
template html content
', + 'path/to/manual/file.md': '## Markdown header', + 'path/to/manual/file.html': '
html file content
' + }; + + patternFiles = { + 'path/to/manual/**/*.!(js|html|md)': [ 'static-file.png' ] + }; + + compileHtmlFiles( { + buildDir: 'buildDir', + language: 'en', + sourceFiles: [ 'path/to/manual/file.js' ] + } ); + + expect( vi.mocked( chokidar ).watch ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( chokidar ).watch ).toHaveBeenCalledWith( 'path/to/manual/file.md', { ignoreInitial: true } ); - expect( stubs.chokidar.watch ).to.be.calledWithExactly( + expect( vi.mocked( chokidar ).watch ).toHaveBeenCalledWith( 'path/to/manual/file.html', { ignoreInitial: true } ); - expect( stubs.fs.copySync ).to.be.calledWithExactly( - 'static-file.png', 'buildDir/static-file.png' - ); + } ); + + it( 'should copy static resources', () => { + files = { + '/template.html': '
template html content
', + 'path/to/manual/file.md': '## Markdown header', + 'path/to/manual/file.html': '
html file content
' + }; + + patternFiles = { + 'path/to/manual/**/*.!(js|html|md)': [ 'static-file.png' ] + }; - expect( stubs.logger.info.callCount ).to.equal( 2 ); - expect( stubs.logger.info.firstCall.args[ 0 ] ).to.match( /^Processing/ ); - expect( stubs.logger.info.secondCall.args[ 0 ] ).to.match( /^Finished writing/ ); + compileHtmlFiles( { + buildDir: 'buildDir', + language: 'en', + sourceFiles: [ 'path/to/manual/file.js' ] + } ); + + expect( vi.mocked( fs ).copySync ).toHaveBeenCalledExactlyOnceWith( 'static-file.png', 'buildDir/static-file.png' ); } ); it( 'should compile files with options#language specified', () => { + files = { + '/template.html': '
template html content
', + 'path/to/manual/file.md': '## Markdown header', + 'path/to/manual/file.html': '
html file content
' + }; + + patternFiles = { + 'path/to/manual/**/*.!(js|html|md)': [] + }; + compileHtmlFiles( { buildDir: 'buildDir', language: 'en', @@ -190,38 +214,36 @@ describe( 'compileHtmlFiles', () => { sourceFiles: [ 'path/to/manual/file.js' ] } ); - /* eslint-disable max-len */ - expect( stubs.fs.outputFileSync ).to.be.calledWithExactly( + expect( vi.mocked( fs ).outputFileSync ).toHaveBeenCalledExactlyOnceWith( 'buildDir/path/to/manual/file.html', [ '
template html content
', '

Markdown header

', '', '
' + - '' + + '' + '', '
html file content
', '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + '' ].join( '\n' ) ); - /* eslint-enable max-len */ } ); it( 'should work with files containing dots in their names', () => { files = { - [ `${ fakeDirname }/template.html` ]: '
template html content
', + '/template.html': '
template html content
', 'path/to/manual/file.abc.md': '## Markdown header', 'path/to/manual/file.abc.html': '
html file content
' }; @@ -235,35 +257,15 @@ describe( 'compileHtmlFiles', () => { sourceFiles: [ 'path/to/manual/file.abc.js' ] } ); - /* eslint-disable max-len */ - expect( stubs.fs.outputFileSync ).to.be.calledWith( - 'buildDir/path/to/manual/file.abc.html', [ - '
template html content
', - '

Markdown header

', - '', - '
' + - '' + - '', - '
html file content
', - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' - ].join( '\n' ) + expect( vi.mocked( fs ).outputFileSync ).toHaveBeenCalledExactlyOnceWith( + 'buildDir/path/to/manual/file.abc.html', + expect.stringContaining( '' ) ); - /* eslint-enable max-len */ } ); it( 'should work with a few entry points patterns', () => { files = { - [ `${ fakeDirname }/template.html` ]: '
template html content
', + '/template.html': '
template html content
', 'path/to/manual/file.md': '## Markdown header', 'path/to/manual/file.html': '
html file content
', 'path/to/another/manual/file.md': '## Markdown header', @@ -283,15 +285,22 @@ describe( 'compileHtmlFiles', () => { ] } ); - expect( stubs.chokidar.watch ).to.be.calledWithExactly( 'path/to/manual/file.md', { ignoreInitial: true } ); - expect( stubs.chokidar.watch ).to.be.calledWithExactly( 'path/to/manual/file.html', { ignoreInitial: true } ); - expect( stubs.chokidar.watch ).to.be.calledWithExactly( 'path/to/another/manual/file.html', { ignoreInitial: true } ); - expect( stubs.chokidar.watch ).to.be.calledWithExactly( 'path/to/another/manual/file.html', { ignoreInitial: true } ); + expect( vi.mocked( fs ).outputFileSync ).toHaveBeenCalledTimes( 2 ); + + expect( vi.mocked( fs ).outputFileSync ).toHaveBeenCalledWith( + 'buildDir/path/to/manual/file.html', + expect.stringContaining( '' ) + ); + + expect( vi.mocked( fs ).outputFileSync ).toHaveBeenCalledWith( + 'buildDir/path/to/another/manual/file.html', + expect.stringContaining( '' ) + ); } ); it( 'should not copy md files containing dots in their file names', () => { files = { - [ `${ fakeDirname }/template.html` ]: '
template html content
', + '/template.html': '
template html content
', 'path/to/manual/file.md': '## Markdown header', 'path/to/manual/file.html': '
html file content
' }; @@ -306,42 +315,12 @@ describe( 'compileHtmlFiles', () => { sourceFiles: [ 'path/to/manual/file.js' ] } ); - expect( stubs.commonmark.parse ).to.be.calledWithExactly( '## Markdown header' ); - expect( stubs.fs.ensureDirSync ).to.be.calledWithExactly( 'buildDir' ); - - /* eslint-disable max-len */ - expect( stubs.fs.outputFileSync ).to.be.calledWithExactly( - 'buildDir/path/to/manual/file.html', [ - '
template html content
', - '

Markdown header

', - '', - '
' + - '' + - '', - '
html file content
', - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' - ].join( '\n' ) - ); - /* eslint-enable max-len */ - - expect( stubs.chokidar.watch ).to.be.calledWithExactly( 'path/to/manual/file.md', { ignoreInitial: true } ); - expect( stubs.chokidar.watch ).to.be.calledWithExactly( 'path/to/manual/file.html', { ignoreInitial: true } ); - expect( stubs.fs.copySync ).not.to.be.calledWith( 'some.file.md', 'buildDir/some.file.md' ); + expect( vi.mocked( fs ).copySync ).not.toHaveBeenCalled(); } ); it( 'should compile the manual test and do not inform about the processed file', () => { files = { - [ `${ fakeDirname }/template.html` ]: '
template html content
', + '/template.html': '
template html content
', 'path/to/manual/file.md': '## Markdown header', 'path/to/manual/file.html': '
html file content
' }; @@ -357,27 +336,22 @@ describe( 'compileHtmlFiles', () => { silent: true } ); - expect( stubs.commonmark.parse ).to.be.calledWithExactly( '## Markdown header' ); - expect( stubs.fs.ensureDirSync ).to.be.calledWithExactly( 'buildDir' ); - - expect( stubs.logger.info.callCount ).to.equal( 0 ); + expect( stubs.log.info ).not.toHaveBeenCalled(); } ); } ); describe( 'Windows environment', () => { beforeEach( () => { separator = '\\'; - compileHtmlFiles = require( '../../../lib/utils/manual-tests/compilehtmlfiles' ); } ); it( 'should work on Windows environments', () => { - // Our wrapper on Glob returns proper paths for Unix and Windows. patternFiles = { - 'path\\to\\manual\\**\\*.!(js|html|md)': [ 'static-file.png' ] + 'path/to/manual/**/*.!(js|html|md)': [ 'static-file.png' ] }; files = { - [ fakeDirname + '\\template.html' ]: '
template html content
', + '\\template.html': '
template html content
', 'path\\to\\manual\\file.md': '## Markdown header', 'path\\to\\manual\\file.html': '
html file content
' }; @@ -387,43 +361,72 @@ describe( 'compileHtmlFiles', () => { sourceFiles: [ 'path\\to\\manual\\file.js' ] } ); - expect( stubs.commonmark.parse ).to.be.calledWithExactly( '## Markdown header' ); - expect( stubs.fs.ensureDirSync ).to.be.calledWithExactly( 'buildDir' ); - - /* eslint-disable max-len */ - expect( stubs.fs.outputFileSync ).to.be.calledWithExactly( + expect( vi.mocked( fs ).outputFileSync ).toHaveBeenCalledExactlyOnceWith( 'buildDir\\path\\to\\manual\\file.html', [ '
template html content
', '

Markdown header

', '', '
' + - '' + + '' + '', '
html file content
', '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + '' ].join( '\n' ) ); - /* eslint-enable max-len */ + } ); - expect( stubs.chokidar.watch ).to.be.calledWithExactly( + it( 'should listen to changes in source files', () => { + patternFiles = { + 'path/to/manual/**/*.!(js|html|md)': [ 'static-file.png' ] + }; + + files = { + '\\template.html': '
template html content
', + 'path\\to\\manual\\file.md': '## Markdown header', + 'path\\to\\manual\\file.html': '
html file content
' + }; + + compileHtmlFiles( { + buildDir: 'buildDir', + sourceFiles: [ 'path\\to\\manual\\file.js' ] + } ); + + expect( vi.mocked( chokidar ).watch ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( chokidar ).watch ).toHaveBeenCalledWith( 'path\\to\\manual\\file.md', { ignoreInitial: true } ); - expect( stubs.chokidar.watch ).to.be.calledWithExactly( + expect( vi.mocked( chokidar ).watch ).toHaveBeenCalledWith( 'path\\to\\manual\\file.html', { ignoreInitial: true } ); - expect( stubs.fs.copySync ).to.be.calledWithExactly( - 'static-file.png', 'buildDir\\static-file.png' - ); + } ); + + it( 'should copy static resources', () => { + patternFiles = { + 'path/to/manual/**/*.!(js|html|md)': [ 'static-file.png' ] + }; + + files = { + '\\template.html': '
template html content
', + 'path\\to\\manual\\file.md': '## Markdown header', + 'path\\to\\manual\\file.html': '
html file content
' + }; + + compileHtmlFiles( { + buildDir: 'buildDir', + sourceFiles: [ 'path\\to\\manual\\file.js' ] + } ); + + expect( vi.mocked( fs ).copySync ).toHaveBeenCalledExactlyOnceWith( 'static-file.png', 'buildDir\\static-file.png' ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/compilescripts.js b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/compilescripts.js index f3323a1cd..5659c7417 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/compilescripts.js +++ b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/compilescripts.js @@ -3,51 +3,40 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import webpack from 'webpack'; +import getRelativeFilePath from '../../../lib/utils/getrelativefilepath.js'; +import requireDll from '../../../lib/utils/requiredll.js'; +import getWebpackConfigForManualTests from '../../../lib/utils/manual-tests/getwebpackconfig.js'; +import compileManualTestScripts from '../../../lib/utils/manual-tests/compilescripts.js'; -const mockery = require( 'mockery' ); -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); +vi.mock( 'webpack' ); +vi.mock( '../../../lib/utils/getrelativefilepath.js' ); +vi.mock( '../../../lib/utils/requiredll.js' ); +vi.mock( '../../../lib/utils/manual-tests/getwebpackconfig.js' ); -describe( 'compileManualTestScripts', () => { - let sandbox, stubs, webpackError, compileManualTestScripts; +describe( 'compileManualTestScripts()', () => { + let webpackError; beforeEach( () => { webpackError = null; - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false + vi.mocked( webpack ).mockImplementation( ( config, callback ) => { + callback( webpackError ); } ); - sandbox = sinon.createSandbox(); - - stubs = { - webpack: sandbox.spy( ( config, callback ) => { - callback( webpackError ); - } ), - getWebpackConfig: sandbox.spy( ( { entries, buildDir } ) => ( { - entries, - buildDir - } ) ), - getRelativeFilePath: sandbox.spy( x => x ), - onTestCompilationStatus: sinon.stub() - }; - - mockery.registerMock( './getwebpackconfig', stubs.getWebpackConfig ); - mockery.registerMock( '../getrelativefilepath', stubs.getRelativeFilePath ); - mockery.registerMock( 'webpack', stubs.webpack ); - - compileManualTestScripts = require( '../../../lib/utils/manual-tests/compilescripts' ); - } ); + vi.mocked( getWebpackConfigForManualTests ).mockImplementation( ( { entries, buildDir } ) => ( { + entries, + buildDir + } ) ); - afterEach( () => { - sandbox.restore(); - mockery.disable(); + vi.mocked( getRelativeFilePath ).mockImplementation( input => input ); } ); it( 'should compile manual test scripts (DLL only)', async () => { + vi.mocked( requireDll ).mockReturnValue( true ); + const onTestCompilationStatus = vi.fn(); + await compileManualTestScripts( { cwd: 'workspace', buildDir: 'buildDir', @@ -57,43 +46,44 @@ describe( 'compileManualTestScripts', () => { ], themePath: 'path/to/theme', language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus, additionalLanguages: [ 'pl', 'ar' ], debug: [ 'CK_DEBUG' ], disableWatch: false } ); - expect( stubs.getWebpackConfig.calledOnce ).to.equal( true ); - - sinon.assert.calledWith( stubs.getWebpackConfig.firstCall, { + expect( vi.mocked( getWebpackConfigForManualTests ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { cwd: 'workspace', requireDll: true, buildDir: 'buildDir', themePath: 'path/to/theme', language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus, additionalLanguages: [ 'pl', 'ar' ], entries: { 'ckeditor5-foo/manual/file1-dll': 'ckeditor5-foo/manual/file1-dll.js', 'ckeditor5-foo/manual/file2-dll': 'ckeditor5-foo/manual/file2-dll.js' }, debug: [ 'CK_DEBUG' ], - disableWatch: false, - identityFile: undefined, - tsconfig: undefined - } ); - - expect( stubs.webpack.calledOnce ).to.equal( true ); - expect( stubs.webpack.firstCall.args[ 0 ] ).to.deep.equal( { - buildDir: 'buildDir', - entries: { - 'ckeditor5-foo/manual/file1-dll': 'ckeditor5-foo/manual/file1-dll.js', - 'ckeditor5-foo/manual/file2-dll': 'ckeditor5-foo/manual/file2-dll.js' - } - } ); + disableWatch: false + } ) ); + + expect( vi.mocked( webpack ) ).toHaveBeenCalledExactlyOnceWith( + { + buildDir: 'buildDir', + entries: { + 'ckeditor5-foo/manual/file1-dll': 'ckeditor5-foo/manual/file1-dll.js', + 'ckeditor5-foo/manual/file2-dll': 'ckeditor5-foo/manual/file2-dll.js' + } + }, + expect.any( Function ) + ); } ); it( 'should compile manual test scripts (non-DLL only)', async () => { + vi.mocked( requireDll ).mockReturnValue( false ); + const onTestCompilationStatus = vi.fn(); + await compileManualTestScripts( { cwd: 'workspace', buildDir: 'buildDir', @@ -103,43 +93,43 @@ describe( 'compileManualTestScripts', () => { ], themePath: 'path/to/theme', language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus, additionalLanguages: [ 'pl', 'ar' ], debug: [ 'CK_DEBUG' ], disableWatch: false } ); - expect( stubs.getWebpackConfig.calledOnce ).to.equal( true ); - - sinon.assert.calledWith( stubs.getWebpackConfig.firstCall, { + expect( vi.mocked( getWebpackConfigForManualTests ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { cwd: 'workspace', requireDll: false, buildDir: 'buildDir', themePath: 'path/to/theme', language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus, additionalLanguages: [ 'pl', 'ar' ], entries: { 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js', 'ckeditor5-foo/manual/file2': 'ckeditor5-foo/manual/file2.js' }, debug: [ 'CK_DEBUG' ], - disableWatch: false, - identityFile: undefined, - tsconfig: undefined - } ); - - expect( stubs.webpack.calledOnce ).to.equal( true ); - expect( stubs.webpack.firstCall.args[ 0 ] ).to.deep.equal( { - buildDir: 'buildDir', - entries: { - 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js', - 'ckeditor5-foo/manual/file2': 'ckeditor5-foo/manual/file2.js' - } - } ); + disableWatch: false + } ) ); + + expect( vi.mocked( webpack ) ).toHaveBeenCalledExactlyOnceWith( + { + buildDir: 'buildDir', + entries: { + 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js', + 'ckeditor5-foo/manual/file2': 'ckeditor5-foo/manual/file2.js' + } + }, + expect.any( Function ) + ); } ); it( 'should compile manual test scripts (DLL and non-DLL)', async () => { + vi.mocked( requireDll ).mockImplementation( input => input.includes( 'dll' ) ); + await compileManualTestScripts( { cwd: 'workspace', buildDir: 'buildDir', @@ -149,66 +139,19 @@ describe( 'compileManualTestScripts', () => { ], themePath: 'path/to/theme', language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus: vi.fn(), additionalLanguages: [ 'pl', 'ar' ], debug: [ 'CK_DEBUG' ], disableWatch: false } ); - expect( stubs.getWebpackConfig.calledTwice ).to.equal( true ); - - sinon.assert.calledWith( stubs.getWebpackConfig.firstCall, { - cwd: 'workspace', - requireDll: true, - buildDir: 'buildDir', - themePath: 'path/to/theme', - language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, - additionalLanguages: [ 'pl', 'ar' ], - entries: { - 'ckeditor5-foo/manual/file2-dll': 'ckeditor5-foo/manual/file2-dll.js' - }, - debug: [ 'CK_DEBUG' ], - disableWatch: false, - identityFile: undefined, - tsconfig: undefined - } ); - - sinon.assert.calledWith( stubs.getWebpackConfig.secondCall, { - cwd: 'workspace', - requireDll: false, - buildDir: 'buildDir', - themePath: 'path/to/theme', - language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, - additionalLanguages: [ 'pl', 'ar' ], - entries: { - 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js' - }, - debug: [ 'CK_DEBUG' ], - disableWatch: false, - identityFile: undefined, - tsconfig: undefined - } ); - - expect( stubs.webpack.calledTwice ).to.equal( true ); - - expect( stubs.webpack.firstCall.args[ 0 ] ).to.deep.equal( { - buildDir: 'buildDir', - entries: { - 'ckeditor5-foo/manual/file2-dll': 'ckeditor5-foo/manual/file2-dll.js' - } - } ); - - expect( stubs.webpack.secondCall.args[ 0 ] ).to.deep.equal( { - buildDir: 'buildDir', - entries: { - 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js' - } - } ); + expect( vi.mocked( getWebpackConfigForManualTests ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( webpack ) ).toHaveBeenCalledTimes( 2 ); } ); it( 'should compile multiple manual test scripts', async () => { + vi.mocked( requireDll ).mockReturnValue( false ); + await compileManualTestScripts( { cwd: 'workspace', buildDir: 'buildDir', @@ -219,26 +162,25 @@ describe( 'compileManualTestScripts', () => { ], themePath: 'path/to/theme', language: null, - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus: vi.fn(), additionalLanguages: null, tsconfig: undefined } ); - expect( stubs.getWebpackConfig.calledOnce ).to.equal( true ); - - expect( stubs.getRelativeFilePath.calledThrice ).to.equal( true ); - expect( stubs.getRelativeFilePath.firstCall.args[ 0 ] ) - .to.equal( 'ckeditor5-build-classic/tests/manual/ckeditor.js' ); - expect( stubs.getRelativeFilePath.secondCall.args[ 0 ] ) - .to.equal( 'ckeditor5-build-classic/tests/manual/ckeditor.compcat.js' ); - expect( stubs.getRelativeFilePath.thirdCall.args[ 0 ] ) - .to.equal( 'ckeditor5-editor-classic/tests/manual/classic.js' ); + expect( vi.mocked( getWebpackConfigForManualTests ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( getRelativeFilePath ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( getRelativeFilePath ) ).toHaveBeenCalledWith( 'ckeditor5-build-classic/tests/manual/ckeditor.js' ); + expect( vi.mocked( getRelativeFilePath ) ).toHaveBeenCalledWith( 'ckeditor5-build-classic/tests/manual/ckeditor.compcat.js' ); + expect( vi.mocked( getRelativeFilePath ) ).toHaveBeenCalledWith( 'ckeditor5-editor-classic/tests/manual/classic.js' ); } ); - it( 'rejects if webpack threw an error', () => { - webpackError = new Error( 'Unexpected error.' ); + it( 'rejects if webpack threw an error', async () => { + vi.mocked( webpack ).mockImplementation( () => { + throw new Error( 'Unexpected error' ); + } ); + vi.mocked( requireDll ).mockReturnValue( false ); - return compileManualTestScripts( { + await expect( compileManualTestScripts( { buildDir: 'buildDir', sourceFiles: [ 'ckeditor5-foo/manual/file1.js', @@ -246,16 +188,9 @@ describe( 'compileManualTestScripts', () => { ], themePath: 'path/to/theme', language: null, - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus: vi.fn(), additionalLanguages: null - } ).then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - err => { - expect( err ).to.equal( webpackError ); - } - ); + } ) ).rejects.toThrow( 'Unexpected error' ); } ); it( 'works on Windows environments', async () => { @@ -266,12 +201,11 @@ describe( 'compileManualTestScripts', () => { ], themePath: 'path/to/theme', language: null, - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus: vi.fn(), additionalLanguages: null } ); - expect( stubs.getRelativeFilePath.calledOnce ).to.equal( true ); - expect( stubs.getRelativeFilePath.firstCall.args[ 0 ] ).to.equal( 'ckeditor5-build-classic\\tests\\manual\\ckeditor.js' ); + expect( vi.mocked( getRelativeFilePath ) ).toHaveBeenCalledExactlyOnceWith( 'ckeditor5-build-classic\\tests\\manual\\ckeditor.js' ); } ); it( 'should pass identity file to webpack configuration factory', async () => { @@ -286,41 +220,16 @@ describe( 'compileManualTestScripts', () => { ], themePath: 'path/to/theme', language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus: vi.fn(), additionalLanguages: [ 'pl', 'ar' ], debug: [ 'CK_DEBUG' ], identityFile, disableWatch: false } ); - expect( stubs.getWebpackConfig.calledOnce ).to.equal( true ); - - sinon.assert.calledWith( stubs.getWebpackConfig.firstCall, { - cwd: 'workspace', - requireDll: false, - buildDir: 'buildDir', - themePath: 'path/to/theme', - language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, - additionalLanguages: [ 'pl', 'ar' ], - entries: { - 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js', - 'ckeditor5-foo/manual/file2': 'ckeditor5-foo/manual/file2.js' - }, - debug: [ 'CK_DEBUG' ], - identityFile, - disableWatch: false, - tsconfig: undefined - } ); - - expect( stubs.webpack.calledOnce ).to.equal( true ); - expect( stubs.webpack.firstCall.args[ 0 ] ).to.deep.equal( { - buildDir: 'buildDir', - entries: { - 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js', - 'ckeditor5-foo/manual/file2': 'ckeditor5-foo/manual/file2.js' - } - } ); + expect( vi.mocked( getWebpackConfigForManualTests ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + identityFile + } ) ); } ); it( 'should pass the "disableWatch" option to webpack configuration factory', async () => { @@ -332,38 +241,15 @@ describe( 'compileManualTestScripts', () => { ], themePath: 'path/to/theme', language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus: vi.fn(), additionalLanguages: [ 'pl', 'ar' ], debug: [ 'CK_DEBUG' ], disableWatch: true } ); - expect( stubs.getWebpackConfig.calledOnce ).to.equal( true ); - - sinon.assert.calledWith( stubs.getWebpackConfig.firstCall, { - cwd: 'workspace', - requireDll: false, - buildDir: 'buildDir', - themePath: 'path/to/theme', - language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, - additionalLanguages: [ 'pl', 'ar' ], - entries: { - 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js' - }, - debug: [ 'CK_DEBUG' ], - identityFile: undefined, - disableWatch: true, - tsconfig: undefined - } ); - - expect( stubs.webpack.calledOnce ).to.equal( true ); - expect( stubs.webpack.firstCall.args[ 0 ] ).to.deep.equal( { - buildDir: 'buildDir', - entries: { - 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js' - } - } ); + expect( vi.mocked( getWebpackConfigForManualTests ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + disableWatch: true + } ) ); } ); it( 'should pass correct entries object to the webpack for both JS and TS files', async () => { @@ -381,21 +267,18 @@ describe( 'compileManualTestScripts', () => { disableWatch: false } ); - expect( stubs.getWebpackConfig.calledOnce ).to.equal( true ); - expect( stubs.getWebpackConfig.firstCall.args[ 0 ] ).to.deep.include( { + expect( vi.mocked( getWebpackConfigForManualTests ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { entries: { 'ckeditor5-foo\\manual\\file1': 'ckeditor5-foo\\manual\\file1.js', 'ckeditor5-foo\\manual\\file2': 'ckeditor5-foo\\manual\\file2.ts' } - } ); - - expect( stubs.webpack.calledOnce ).to.equal( true ); - expect( stubs.webpack.firstCall.args[ 0 ] ).to.deep.include( { + } ) ); + expect( vi.mocked( webpack ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { entries: { 'ckeditor5-foo\\manual\\file1': 'ckeditor5-foo\\manual\\file1.js', 'ckeditor5-foo\\manual\\file2': 'ckeditor5-foo\\manual\\file2.ts' } - } ); + } ), expect.any( Function ) ); } ); it( 'should pass the "tsconfig" option to webpack configuration factory', async () => { @@ -407,37 +290,14 @@ describe( 'compileManualTestScripts', () => { ], themePath: 'path/to/theme', language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, + onTestCompilationStatus: vi.fn(), additionalLanguages: [ 'pl', 'ar' ], debug: [ 'CK_DEBUG' ], tsconfig: '/absolute/path/to/tsconfig.json' } ); - expect( stubs.getWebpackConfig.calledOnce ).to.equal( true ); - - sinon.assert.calledWith( stubs.getWebpackConfig.firstCall, { - cwd: 'workspace', - requireDll: false, - buildDir: 'buildDir', - themePath: 'path/to/theme', - language: 'en', - onTestCompilationStatus: stubs.onTestCompilationStatus, - additionalLanguages: [ 'pl', 'ar' ], - entries: { - 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js' - }, - debug: [ 'CK_DEBUG' ], - identityFile: undefined, - disableWatch: undefined, + expect( vi.mocked( getWebpackConfigForManualTests ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { tsconfig: '/absolute/path/to/tsconfig.json' - } ); - - expect( stubs.webpack.calledOnce ).to.equal( true ); - expect( stubs.webpack.firstCall.args[ 0 ] ).to.deep.equal( { - buildDir: 'buildDir', - entries: { - 'ckeditor5-foo/manual/file1': 'ckeditor5-foo/manual/file1.js' - } - } ); + } ) ); } ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/createserver.js b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/createserver.js index 44676ef51..3bf525743 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/createserver.js +++ b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/createserver.js @@ -3,92 +3,368 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import http from 'http'; +import readline from 'readline'; +import fs from 'fs'; +import { globSync } from 'glob'; +import combine from 'dom-combiner'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import createManualTestServer from '../../../lib/utils/manual-tests/createserver.js'; -const http = require( 'http' ); -const { use, expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const sinonChai = require( 'sinon-chai' ); -const mockery = require( 'mockery' ); +vi.mock( 'readline' ); +vi.mock( 'fs' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( 'glob' ); +vi.mock( 'dom-combiner' ); -use( sinonChai ); +describe( 'createManualTestServer()', () => { + let loggerStub, server; -describe( 'createManualTestServer', () => { - let sandbox, httpCreateServerStub, createManualTestServer, server, loggerStub; + beforeEach( async () => { + const { createServer } = http; - beforeEach( () => { - sandbox = sinon.createSandbox(); + loggerStub = vi.fn(); - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false + vi.mocked( logger ).mockReturnValue( { + info: loggerStub } ); - loggerStub = sinon.stub(); + vi.spyOn( http, 'createServer' ).mockImplementation( ( ...theArgs ) => { + server = createServer( ...theArgs ); - httpCreateServerStub = sandbox.stub( http, 'createServer' ).callsFake( function stubbedCreateServer( ...theArgs ) { - server = httpCreateServerStub.wrappedMethod( ...theArgs ); - sandbox.spy( server, 'listen' ); + vi.spyOn( server, 'listen' ); return server; } ); - - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { - logger() { - return { - info: loggerStub - }; - } - } ); - - createManualTestServer = require( '../../../lib/utils/manual-tests/createserver' ); } ); afterEach( () => { server.close(); - sandbox.restore(); - - // To avoid false positives and encourage better testing practices, Mocha will no longer automatically - // kill itself via `process.exit()` when it thinks it should be done running. Hence, we must close the stream - // before leaving the test. See: https://stackoverflow.com/a/52143003. - if ( server._readline ) { - server._readline.close(); - } - - mockery.disable(); } ); it( 'should start http server', () => { createManualTestServer( 'workspace/build/.manual-tests' ); - expect( httpCreateServerStub ).to.be.calledOnce; + expect( vi.mocked( http ).createServer ).toHaveBeenCalledOnce(); } ); it( 'should listen on given port', () => { createManualTestServer( 'workspace/build/.manual-tests', 8888 ); - expect( httpCreateServerStub.returnValues[ 0 ].listen ).to.be.calledOnceWith( 8888 ); + expect( server ).toEqual( expect.objectContaining( { + listen: expect.any( Function ) + } ) ); - expect( loggerStub.calledOnce ).to.equal( true ); - expect( loggerStub.firstCall.firstArg ).to.equal( '[Server] Server running at http://localhost:8888/' ); + expect( server.listen ).toHaveBeenCalledExactlyOnceWith( 8888 ); + expect( loggerStub ).toHaveBeenCalledExactlyOnceWith( '[Server] Server running at http://localhost:8888/' ); } ); it( 'should listen on 8125 port if no specific port was given', () => { createManualTestServer( 'workspace/build/.manual-tests' ); - expect( httpCreateServerStub.returnValues[ 0 ].listen ).to.be.calledOnceWith( 8125 ); + expect( server ).toEqual( expect.objectContaining( { + listen: expect.any( Function ) + } ) ); - expect( loggerStub.calledOnce ).to.equal( true ); - expect( loggerStub.firstCall.firstArg ).to.equal( '[Server] Server running at http://localhost:8125/' ); + expect( server.listen ).toHaveBeenCalledExactlyOnceWith( 8125 ); + expect( loggerStub ).toHaveBeenCalledExactlyOnceWith( '[Server] Server running at http://localhost:8125/' ); } ); - it( 'should call the specificed callback when the server is running (e.g. to allow running web sockets)', () => { - const spy = sinon.spy(); + it( 'should call the specified callback when the server is running (e.g. to allow running web sockets)', () => { + const spy = vi.fn(); createManualTestServer( 'workspace/build/.manual-tests', 1234, spy ); - sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, server ); + expect( spy ).toHaveBeenCalledExactlyOnceWith( server ); + } ); + + it( 'should use "readline" to listen to the SIGINT event on Windows', () => { + const readlineInterface = { + on: vi.fn() + }; + + vi.mocked( readline ).createInterface.mockReturnValue( readlineInterface ); + vi.spyOn( process, 'platform', 'get' ).mockReturnValue( 'win32' ); + + createManualTestServer( 'workspace/build/.manual-tests' ); + + expect( vi.mocked( readline ).createInterface ).toHaveBeenCalledOnce(); + expect( readlineInterface.on ).toHaveBeenCalledExactlyOnceWith( 'SIGINT', expect.any( Function ) ); + } ); + + describe( 'request handler', () => { + beforeEach( () => { + createManualTestServer( 'workspace/build/.manual-tests' ); + } ); + + it( 'should handle a request for a favicon (`/favicon.ico`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/favicon.ico' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'image/x-icon' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( null, 'utf-8' ); + } ); + + it( 'should handle a root request (`/`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( '' ); + vi.mocked( globSync ).mockReturnValue( [ + '/build/.manual-tests/ckeditor5-foo/tests/manual/test-1.html', + '/build/.manual-tests/ckeditor5-foo/tests/manual/test-2.html', + '/build/.manual-tests/ckeditor5-bar/tests/manual/test-3.html', + '/build/.manual-tests/ckeditor5-bar/tests/manual/test-4.html' + ] ); + vi.mocked( combine ).mockReturnValue( 'Generated index.' ); + + serverCallback( request, response ); + + const expectedTestList = + '
'; + + expect( vi.mocked( combine ) ).toHaveBeenCalledExactlyOnceWith( + '', + expect.stringContaining( 'CKEditor 5 manual tests' ), + expectedTestList + ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'text/html' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'Generated index.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.html`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.html' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'text/html' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.js`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.js' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'text/javascript' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.json`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.json' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'application/json' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.js.map`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.js.map' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'application/json' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.css`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.css' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'text/css' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.png`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.png' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'image/png' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a static resource (`*.jpg`)', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.jpg' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( fs ).readFileSync.mockReturnValue( 'An example content.' ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 200, expect.objectContaining( { + 'Content-Type': 'image/jpg' + } ) ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( 'An example content.', 'utf-8' ); + } ); + + it( 'should handle a request for a non-existing resource', () => { + const [ firstCall ] = vi.mocked( http ).createServer.mock.calls; + const [ serverCallback ] = firstCall; + + const request = { + url: '/file.jpg' + }; + const response = { + writeHead: vi.fn(), + end: vi.fn() + }; + + vi.mocked( logger ).mockReturnValue( { + error: vi.fn() + } ); + + vi.mocked( fs ).readFileSync.mockImplementation( () => { + const error = new Error( 'A resource does not exist' ); + error.code = 'ENOENT'; + + throw error; + } ); + + serverCallback( request, response ); + + expect( response.writeHead ).toHaveBeenCalledExactlyOnceWith( 404 ); + + expect( response.end ).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining( 'Sorry, check with the site admin for error: ENOENT' ), + 'utf-8' + ); + } ); } ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/getwebpackconfig.js b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/getwebpackconfig.js index cf1f40ddb..01ed4c270 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/getwebpackconfig.js +++ b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/getwebpackconfig.js @@ -3,72 +3,46 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const mockery = require( 'mockery' ); -const sinon = require( 'sinon' ); -const { expect } = require( 'chai' ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { loaders } from '@ckeditor/ckeditor5-dev-utils'; +import getDefinitionsFromFile from '../../../lib/utils/getdefinitionsfromfile.js'; +import getWebpackConfigForManualTests from '../../../lib/utils/manual-tests/getwebpackconfig.js'; + +const stubs = vi.hoisted( () => ( { + translations: { + plugin: { + constructor: vi.fn() + } + } +} ) ); + +vi.mock( 'webpack' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( '@ckeditor/ckeditor5-dev-translations', () => ( { + CKEditorTranslationsPlugin: class CKEditorTranslationsPlugin { + constructor( ...args ) { + stubs.translations.plugin.constructor( ...args ); + } + } +} ) ); +vi.mock( '../../../lib/utils/getdefinitionsfromfile.js' ); describe( 'getWebpackConfigForManualTests()', () => { - let getWebpackConfigForManualTests, stubs; - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - getDefinitionsFromFile: sinon.stub().returns( {} ), - loaders: { - getIconsLoader: sinon.stub().returns( {} ), - getStylesLoader: sinon.stub().returns( {} ), - getTypeScriptLoader: sinon.stub().returns( {} ), - getFormattedTextLoader: sinon.stub().returns( {} ), - getCoverageLoader: sinon.stub().returns( {} ), - getJavaScriptLoader: sinon.stub().returns( {} ) - }, - logger: {}, - webpack: { - DefinePlugin: sinon.stub(), - ProvidePlugin: sinon.stub(), - SourceMapDevToolPlugin: sinon.stub() - }, - devTranslations: { - CKEditorTranslationsPlugin: class { - constructor( args ) { - this.args = args; - } - } - } - }; - - mockery.registerMock( 'webpack', stubs.webpack ); - - mockery.registerMock( '@ckeditor/ckeditor5-dev-translations', stubs.devTranslations ); - - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { - loaders: stubs.loaders, - logger: () => stubs.logger - } ); - - getWebpackConfigForManualTests = require( '../../../lib/utils/manual-tests/getwebpackconfig' ); - } ); - - afterEach( () => { - sinon.restore(); - mockery.disable(); - mockery.deregisterAll(); + vi.mocked( getDefinitionsFromFile ).mockReturnValue( {} ); + vi.mocked( loaders ).getIconsLoader.mockReturnValue( {} ); + vi.mocked( loaders ).getStylesLoader.mockReturnValue( {} ); + vi.mocked( loaders ).getTypeScriptLoader.mockReturnValue( {} ); + vi.mocked( loaders ).getFormattedTextLoader.mockReturnValue( {} ); + vi.mocked( loaders ).getCoverageLoader.mockReturnValue( {} ); + vi.mocked( loaders ).getJavaScriptLoader.mockReturnValue( {} ); } ); it( 'should return webpack configuration object', () => { const entries = { 'ckeditor5/tests/manual/all-features': '/home/ckeditor/ckeditor5/tests/manual/all-features.js' }; - const buildDir = '/home/ckeditor/ckeditor5/build/.manual-tests'; - const debug = []; const webpackConfig = getWebpackConfigForManualTests( { @@ -79,37 +53,42 @@ describe( 'getWebpackConfigForManualTests()', () => { tsconfig: '/tsconfig/path' } ); - expect( stubs.loaders.getIconsLoader.calledOnce ).to.equal( true ); - expect( stubs.loaders.getIconsLoader.firstCall.args[ 0 ] ).to.have.property( 'matchExtensionOnly', true ); - - expect( stubs.loaders.getStylesLoader.calledOnce ).to.equal( true ); - expect( stubs.loaders.getStylesLoader.firstCall.args[ 0 ] ).to.have.property( 'themePath', '/theme/path' ); - expect( stubs.loaders.getStylesLoader.firstCall.args[ 0 ] ).to.have.property( 'sourceMap', true ); + expect( vi.mocked( loaders ).getIconsLoader ).toHaveBeenCalledExactlyOnceWith( { + matchExtensionOnly: true + } ); + expect( vi.mocked( loaders ).getStylesLoader ).toHaveBeenCalledExactlyOnceWith( { + themePath: '/theme/path', + sourceMap: true + } ); - expect( stubs.loaders.getTypeScriptLoader.calledOnce ).to.equal( true ); + expect( vi.mocked( loaders ).getTypeScriptLoader ).toHaveBeenCalledExactlyOnceWith( { + debugFlags: debug, + configFile: '/tsconfig/path', + includeDebugLoader: true + } ); - expect( stubs.loaders.getTypeScriptLoader.firstCall.args[ 0 ] ).to.have.property( 'debugFlags', debug ); - expect( stubs.loaders.getTypeScriptLoader.firstCall.args[ 0 ] ).to.have.property( 'configFile', '/tsconfig/path' ); - expect( stubs.loaders.getTypeScriptLoader.firstCall.args[ 0 ] ).to.have.property( 'includeDebugLoader', true ); + expect( vi.mocked( loaders ).getFormattedTextLoader ).toHaveBeenCalledOnce(); - expect( stubs.loaders.getFormattedTextLoader.calledOnce ).to.equal( true ); + expect( vi.mocked( loaders ).getJavaScriptLoader ).toHaveBeenCalledExactlyOnceWith( { + debugFlags: debug + } ); - expect( stubs.loaders.getJavaScriptLoader.calledOnce ).to.equal( true ); - expect( stubs.loaders.getJavaScriptLoader.firstCall.args[ 0 ] ).to.have.property( 'debugFlags', debug ); + expect( vi.mocked( loaders ).getCoverageLoader ).not.toHaveBeenCalledOnce(); - expect( stubs.loaders.getCoverageLoader.called ).to.equal( false ); + expect( webpackConfig ).toEqual( expect.objectContaining( { + // To avoid "eval()" in files. + mode: 'none', + entry: entries, + output: { + path: buildDir + }, + plugins: expect.any( Array ), + watch: true, + resolve: expect.any( Object ) + } ) ); - expect( webpackConfig ).to.be.an( 'object' ); expect( webpackConfig.resolve.fallback.timers ).to.equal( false ); - // To avoid "eval()" in files. - expect( webpackConfig ).to.have.property( 'mode', 'none' ); - expect( webpackConfig ).to.have.property( 'entry', entries ); - expect( webpackConfig ).to.have.property( 'output' ); - expect( webpackConfig.output ).to.deep.equal( { path: buildDir } ); - expect( webpackConfig ).to.have.property( 'plugins' ); - expect( webpackConfig ).to.have.property( 'watch', true ); - // The `devtool` property has been replaced by the `SourceMapDevToolPlugin()`. expect( webpackConfig ).to.not.have.property( 'devtool' ); } ); @@ -125,12 +104,15 @@ describe( 'getWebpackConfigForManualTests()', () => { it( 'pattern passed to CKEditorTranslationsPlugin should match paths to ckeditor5 packages', () => { const webpackConfig = getWebpackConfigForManualTests( { disableWatch: true } ); - expect( webpackConfig ).to.have.property( 'plugins' ); - expect( webpackConfig.plugins ).to.be.an( 'Array' ); + expect( stubs.translations.plugin.constructor ).toHaveBeenCalledOnce(); const CKEditorTranslationsPlugin = webpackConfig.plugins.find( plugin => plugin.constructor.name === 'CKEditorTranslationsPlugin' ); - const pattern = CKEditorTranslationsPlugin.args.packageNamesPattern; + expect( CKEditorTranslationsPlugin ).toBeTruthy(); + + const [ firstCall ] = stubs.translations.plugin.constructor.mock.calls; + const [ firstArg ] = firstCall; + const { packageNamesPattern: pattern } = firstArg; expect( 'packages/ckeditor5-foo/bar'.match( pattern )[ 0 ] ).to.equal( 'packages/ckeditor5-foo/' ); } ); @@ -138,12 +120,15 @@ describe( 'getWebpackConfigForManualTests()', () => { it( 'pattern passed to CKEditorTranslationsPlugin should match paths to external repositories named like ckeditor5 package', () => { const webpackConfig = getWebpackConfigForManualTests( { disableWatch: true } ); - expect( webpackConfig ).to.have.property( 'plugins' ); - expect( webpackConfig.plugins ).to.be.an( 'Array' ); + expect( stubs.translations.plugin.constructor ).toHaveBeenCalledOnce(); const CKEditorTranslationsPlugin = webpackConfig.plugins.find( plugin => plugin.constructor.name === 'CKEditorTranslationsPlugin' ); - const pattern = CKEditorTranslationsPlugin.args.packageNamesPattern; + expect( CKEditorTranslationsPlugin ).toBeTruthy(); + + const [ firstCall ] = stubs.translations.plugin.constructor.mock.calls; + const [ firstArg ] = firstCall; + const { packageNamesPattern: pattern } = firstArg; expect( 'external/ckeditor5-foo/packages/ckeditor5-bar/baz'.match( pattern )[ 0 ] ).to.equal( 'packages/ckeditor5-bar/' ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/removedir.js b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/removedir.js index 1ea41d208..dd07059f8 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/manual-tests/removedir.js +++ b/packages/ckeditor5-dev-tests/tests/utils/manual-tests/removedir.js @@ -3,76 +3,46 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const mockery = require( 'mockery' ); -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); - -describe( 'removeDir', () => { - let sandbox, removeDir; - const logMessages = []; - const deletedPaths = []; - - before( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { - logger: () => ( { - info( message ) { - logMessages.push( message ); - } - } ) - } ); - - mockery.registerMock( 'del', path => { - return Promise.resolve().then( () => { - deletedPaths.push( path ); - } ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { deleteAsync } from 'del'; +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import chalk from 'chalk'; +import removeDir from '../../../lib/utils/manual-tests/removedir.js'; + +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( 'del' ); +vi.mock( 'chalk', () => ( { + default: { + cyan: vi.fn() + } +} ) ); + +describe( 'removeDir()', () => { + let logInfo; + beforeEach( () => { + logInfo = vi.fn(); + + vi.mocked( deleteAsync ).mockResolvedValue(); + vi.mocked( chalk ).cyan.mockImplementation( input => input ); + vi.mocked( logger ).mockReturnValue( { + info: logInfo } ); - - mockery.registerMock( 'chalk', { - cyan: message => `\u001b[36m${ message }\u001b[39m` - } ); - - removeDir = require( '../../../lib/utils/manual-tests/removedir' ); - sandbox = sinon.createSandbox(); } ); - after( () => { - mockery.disable(); - mockery.deregisterAll(); - } ); + it( 'should remove directory and log it', async () => { + await removeDir( 'workspace/directory' ); - afterEach( () => { - sandbox.restore(); - logMessages.length = 0; - deletedPaths.length = 0; + expect( vi.mocked( chalk ).cyan ).toHaveBeenCalledOnce(); + expect( vi.mocked( deleteAsync ) ).toHaveBeenCalledExactlyOnceWith( 'workspace/directory' ); + expect( logInfo ).toHaveBeenCalledExactlyOnceWith( 'Removed directory \'workspace/directory\'' ); } ); - it( 'should remove directory and log it', () => { - return removeDir( 'workspace/directory' ).then( () => { - expect( logMessages ).to.deep.equal( [ - 'Removed directory \'\u001b[36mworkspace/directory\u001b[39m\'' - ] ); + it( 'should remove directory and does not inform about it', async () => { + await removeDir( 'workspace/directory', { silent: true } ); - expect( deletedPaths ).to.deep.equal( [ - 'workspace/directory' - ] ); - } ); - } ); - - it( 'should remove directory and does not inform about it', () => { - return removeDir( 'workspace/directory', { silent: true } ).then( () => { - expect( logMessages ).to.deep.equal( [] ); + expect( vi.mocked( deleteAsync ) ).toHaveBeenCalledExactlyOnceWith( 'workspace/directory' ); - expect( deletedPaths ).to.deep.equal( [ - 'workspace/directory' - ] ); - } ); + expect( vi.mocked( chalk ).cyan ).not.toHaveBeenCalled(); + expect( logInfo ).not.toHaveBeenCalled(); } ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/requiredll.js b/packages/ckeditor5-dev-tests/tests/utils/requiredll.js index b4a13f5ef..a0e129973 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/requiredll.js +++ b/packages/ckeditor5-dev-tests/tests/utils/requiredll.js @@ -3,103 +3,83 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it } from 'vitest'; +import requireDll from '../../lib/utils/requiredll.js'; -const sinon = require( 'sinon' ); -const { expect } = require( 'chai' ); +describe( 'requireDll()', () => { + it( 'should return true when loads JavaScript DLL file (Unix)', () => { + const files = [ + '/workspace/ckeditor5/tests/manual/all-features-dll.js' + ]; -describe( 'dev-tests/utils', () => { - let requireDll; - - beforeEach( () => { - requireDll = require( '../../lib/utils/requiredll' ); + expect( requireDll( files ) ).to.equal( true ); } ); - describe( 'requireDll()', () => { - let sandbox; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - } ); - - afterEach( () => { - sandbox.restore(); - } ); - - it( 'should return true when loads JavaScript DLL file (Unix)', () => { - const files = [ - '/workspace/ckeditor5/tests/manual/all-features-dll.js' - ]; + it( 'should return true when loads single JavaScript DLL file (Unix)', () => { + const file = '/workspace/ckeditor5/tests/manual/all-features-dll.js'; - expect( requireDll( files ) ).to.equal( true ); - } ); - - it( 'should return true when loads single JavaScript DLL file (Unix)', () => { - const file = '/workspace/ckeditor5/tests/manual/all-features-dll.js'; - - expect( requireDll( file ) ).to.equal( true ); - } ); + expect( requireDll( file ) ).to.equal( true ); + } ); - it( 'should return true when loads TypeScript DLL file (Unix)', () => { - const files = [ - '/workspace/ckeditor5/tests/manual/all-features-dll.ts' - ]; + it( 'should return true when loads TypeScript DLL file (Unix)', () => { + const files = [ + '/workspace/ckeditor5/tests/manual/all-features-dll.ts' + ]; - expect( requireDll( files ) ).to.equal( true ); - } ); + expect( requireDll( files ) ).to.equal( true ); + } ); - it( 'should return true when loads JavaScript DLL file (Windows)', () => { - const files = [ - 'C:\\workspace\\ckeditor5\\tests\\manual\\all-features-dll.js' - ]; + it( 'should return true when loads JavaScript DLL file (Windows)', () => { + const files = [ + 'C:\\workspace\\ckeditor5\\tests\\manual\\all-features-dll.js' + ]; - expect( requireDll( files ) ).to.equal( true ); - } ); + expect( requireDll( files ) ).to.equal( true ); + } ); - it( 'should return true when loads TypeScript DLL file (Windows)', () => { - const files = [ - 'C:\\workspace\\ckeditor5\\tests\\manual\\all-features-dll.ts' - ]; + it( 'should return true when loads TypeScript DLL file (Windows)', () => { + const files = [ + 'C:\\workspace\\ckeditor5\\tests\\manual\\all-features-dll.ts' + ]; - expect( requireDll( files ) ).to.equal( true ); - } ); + expect( requireDll( files ) ).to.equal( true ); + } ); - it( 'should return false when loads JavaScript non-DLL file (Unix)', () => { - const files = [ - '/workspace/ckeditor5/tests/manual/article.js' - ]; + it( 'should return false when loads JavaScript non-DLL file (Unix)', () => { + const files = [ + '/workspace/ckeditor5/tests/manual/article.js' + ]; - expect( requireDll( files ) ).to.equal( false ); - } ); + expect( requireDll( files ) ).to.equal( false ); + } ); - it( 'should return false when loads single JavaScript non-DLL file (Unix)', () => { - const file = '/workspace/ckeditor5/tests/manual/article.js'; + it( 'should return false when loads single JavaScript non-DLL file (Unix)', () => { + const file = '/workspace/ckeditor5/tests/manual/article.js'; - expect( requireDll( file ) ).to.equal( false ); - } ); + expect( requireDll( file ) ).to.equal( false ); + } ); - it( 'should return false when loads TypeScript non-DLL file (Unix)', () => { - const files = [ - '/workspace/ckeditor5/tests/manual/article.ts' - ]; + it( 'should return false when loads TypeScript non-DLL file (Unix)', () => { + const files = [ + '/workspace/ckeditor5/tests/manual/article.ts' + ]; - expect( requireDll( files ) ).to.equal( false ); - } ); + expect( requireDll( files ) ).to.equal( false ); + } ); - it( 'should return false when loads JavaScript non-DLL file (Windows)', () => { - const files = [ - 'C:\\workspace\\ckeditor5\\tests\\manual\\article.js' - ]; + it( 'should return false when loads JavaScript non-DLL file (Windows)', () => { + const files = [ + 'C:\\workspace\\ckeditor5\\tests\\manual\\article.js' + ]; - expect( requireDll( files ) ).to.equal( false ); - } ); + expect( requireDll( files ) ).to.equal( false ); + } ); - it( 'should return false when loads TypeScript non-DLL file (Windows)', () => { - const files = [ - 'C:\\workspace\\ckeditor5\\tests\\manual\\article.ts' - ]; + it( 'should return false when loads TypeScript non-DLL file (Windows)', () => { + const files = [ + 'C:\\workspace\\ckeditor5\\tests\\manual\\article.ts' + ]; - expect( requireDll( files ) ).to.equal( false ); - } ); + expect( requireDll( files ) ).to.equal( false ); } ); } ); diff --git a/packages/ckeditor5-dev-tests/tests/utils/transformfileoptiontotestglob.js b/packages/ckeditor5-dev-tests/tests/utils/transformfileoptiontotestglob.js index 179494936..c302c926a 100644 --- a/packages/ckeditor5-dev-tests/tests/utils/transformfileoptiontotestglob.js +++ b/packages/ckeditor5-dev-tests/tests/utils/transformfileoptiontotestglob.js @@ -3,30 +3,20 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import path from 'path'; +import fs from 'fs'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import transformFileOptionToTestGlob from '../../lib/utils/transformfileoptiontotestglob.js'; -const path = require( 'path' ); -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const fs = require( 'fs' ); - -describe( 'dev-tests/utils', () => { - let transformFileOptionToTestGlob, sandbox, readdirSyncStub, existsSyncStub, statSyncStub; +vi.mock( 'fs' ); +describe( 'transformFileOptionToTestGlob()', () => { beforeEach( () => { - sandbox = sinon.createSandbox(); - - sandbox.stub( path, 'join' ).callsFake( ( ...chunks ) => chunks.join( '/' ) ); - sandbox.stub( process, 'cwd' ).returns( '/workspace' ); - statSyncStub = sandbox.stub( fs, 'statSync' ).returns( { isDirectory: () => true } ); - readdirSyncStub = sandbox.stub( fs, 'readdirSync' ).returns( [ 'external-directory' ] ); - existsSyncStub = sandbox.stub( fs, 'existsSync' ).returns( true ); - - transformFileOptionToTestGlob = require( '../../lib/utils/transformfileoptiontotestglob' ); - } ); - - afterEach( () => { - sandbox.restore(); + vi.spyOn( path, 'join' ).mockImplementation( ( ...chunks ) => chunks.join( '/' ) ); + vi.spyOn( process, 'cwd' ).mockReturnValue( '/workspace' ); + vi.mocked( fs ).statSync.mockReturnValue( { isDirectory: () => true } ); + vi.mocked( fs ).readdirSync.mockReturnValue( [ 'external-directory' ] ); + vi.mocked( fs ).existsSync.mockReturnValue( true ); } ); describe( 'converts "ckeditor5" to pattern matching all root package tests', () => { @@ -255,7 +245,7 @@ describe( 'dev-tests/utils', () => { describe( 'should return correct glob for external dirs when external dir name passed', () => { it( 'for automated tests', () => { - readdirSyncStub.returns( [ 'test-external-directory' ] ); + vi.mocked( fs ).readdirSync.mockReturnValue( [ 'test-external-directory' ] ); expect( transformFileOptionToTestGlob( 'test-external-directory' ) ).to.deep.equal( [ '/workspace/external/test-external-directory/tests/**/*.{js,ts}' @@ -263,7 +253,7 @@ describe( 'dev-tests/utils', () => { } ); it( 'for manual tests', () => { - readdirSyncStub.returns( [ 'test-external-directory' ] ); + vi.mocked( fs ).readdirSync.mockReturnValue( [ 'test-external-directory' ] ); expect( transformFileOptionToTestGlob( 'test-external-directory', true ) ).to.deep.equal( [ '/workspace/external/test-external-directory/tests/manual/**/*.{js,ts}' @@ -271,8 +261,8 @@ describe( 'dev-tests/utils', () => { } ); it( 'should not match external directory when isDirectory returns false', () => { - statSyncStub.returns( { isDirectory: () => false } ); - readdirSyncStub.returns( [ 'test-external-file' ] ); + vi.mocked( fs ).statSync.mockReturnValue( { isDirectory: () => false } ); + vi.mocked( fs ).readdirSync.mockReturnValue( [ 'test-external-file' ] ); expect( transformFileOptionToTestGlob( 'test-external-directory', true ) ).to.deep.equal( [ '/workspace/packages/ckeditor5-test-external-directory/tests/manual/**/*.{js,ts}', @@ -284,10 +274,10 @@ describe( 'dev-tests/utils', () => { } ); it( 'should not call readdirSync if directory does not exist', () => { - existsSyncStub.returns( false ); + vi.mocked( fs ).existsSync.mockReturnValue( false ); transformFileOptionToTestGlob( 'test-random-directory' ); - expect( readdirSyncStub.called ).to.equal( false ); + expect( vi.mocked( fs ).readdirSync ).not.toHaveBeenCalled(); } ); } ); diff --git a/packages/ckeditor5-dev-tests/vitest.config.js b/packages/ckeditor5-dev-tests/vitest.config.js new file mode 100644 index 000000000..450eeda30 --- /dev/null +++ b/packages/ckeditor5-dev-tests/vitest.config.js @@ -0,0 +1,31 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig( { + test: { + setupFiles: [ + './tests/_utils/testsetup.js' + ], + testTimeout: 10000, + mockReset: true, + restoreMocks: true, + include: [ + 'tests/**/*.js' + ], + exclude: [ + './tests/_utils/**/*.js', + './tests/fixtures/**/*.js' + ], + coverage: { + provider: 'v8', + include: [ + 'lib/**' + ], + reporter: [ 'text', 'json', 'html', 'lcov' ] + } + } +} ); diff --git a/packages/ckeditor5-dev-transifex/lib/createpotfiles.js b/packages/ckeditor5-dev-transifex/lib/createpotfiles.js index 26f5aa87d..b7c4eafc6 100644 --- a/packages/ckeditor5-dev-transifex/lib/createpotfiles.js +++ b/packages/ckeditor5-dev-transifex/lib/createpotfiles.js @@ -3,33 +3,33 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import path from 'path'; +import fs from 'fs-extra'; +import { deleteSync } from 'del'; +import { logger as utilsLogger } from '@ckeditor/ckeditor5-dev-utils'; +import { findMessages } from '@ckeditor/ckeditor5-dev-translations'; +import { verifyProperties } from './utils.js'; -const path = require( 'path' ); -const fs = require( 'fs-extra' ); -const del = require( 'del' ); -const defaultLogger = require( '@ckeditor/ckeditor5-dev-utils' ).logger(); -const { findMessages } = require( '@ckeditor/ckeditor5-dev-translations' ); -const { verifyProperties } = require( './utils' ); - -const langContextSuffix = path.join( 'lang', 'contexts.json' ); const corePackageName = 'ckeditor5-core'; /** * Collects i18n messages for all packages using source messages from `t()` calls * and context files and saves them as POT files in the `build/.transifex` directory. * - * @param {Object} options - * @param {Array.} options.sourceFiles An array of source files that contain messages to translate. - * @param {Array.} options.packagePaths An array of paths to packages, which will be used to find message contexts. - * @param {String} options.corePackagePath A relative to `process.cwd()` path to the `@ckeditor/ckeditor5-core` package. - * @param {String} options.translationsDirectory An absolute path to the directory where the results should be saved. - * @param {Boolean} [options.ignoreUnusedCorePackageContexts=false] Whether to hide unused context errors related to + * @param {object} options + * @param {Array.} options.sourceFiles An array of source files that contain messages to translate. + * @param {Array.} options.packagePaths An array of paths to packages, which will be used to find message contexts. + * @param {string} options.corePackagePath A relative to `process.cwd()` path to the `@ckeditor/ckeditor5-core` package. + * @param {string} options.translationsDirectory An absolute path to the directory where the results should be saved. + * @param {boolean} [options.ignoreUnusedCorePackageContexts=false] Whether to hide unused context errors related to * the `@ckeditor/ckeditor5-core` package. - * @param {Boolean} [options.skipLicenseHeader=false] Whether to skip the license header in created `*.pot` files. + * @param {boolean} [options.skipLicenseHeader=false] Whether to skip the license header in created `*.pot` files. * @param {Logger} [options.logger] A logger. */ -module.exports = function createPotFiles( options ) { +export default function createPotFiles( options ) { + const defaultLogger = utilsLogger(); + const langContextSuffix = path.join( 'lang', 'contexts.json' ); + verifyProperties( options, [ 'sourceFiles', 'packagePaths', 'corePackagePath', 'translationsDirectory' ] ); const { @@ -42,12 +42,12 @@ module.exports = function createPotFiles( options ) { logger = defaultLogger } = options; - const packageContexts = getPackageContexts( packagePaths, corePackagePath ); + const packageContexts = getPackageContexts( packagePaths, corePackagePath, langContextSuffix ); const sourceMessages = collectSourceMessages( { sourceFiles, logger } ); const errors = [].concat( assertNoMissingContext( { packageContexts, sourceMessages } ), - assertAllContextUsed( { packageContexts, sourceMessages, ignoreUnusedCorePackageContexts, corePackagePath } ), + assertAllContextUsed( { packageContexts, sourceMessages, ignoreUnusedCorePackageContexts, corePackagePath, langContextSuffix } ), assertNoRepeatedContext( { packageContexts } ) ); @@ -83,23 +83,23 @@ module.exports = function createPotFiles( options ) { translationsDirectory } ); } -}; +} /** * Traverses all packages and returns a map of all found language contexts * (file content and file name). * - * @param {Array.} packagePaths An array of paths to packages, which will be used to find message contexts. - * @returns {Map.} + * @param {Array.} packagePaths An array of paths to packages, which will be used to find message contexts. + * @returns {Map.} */ -function getPackageContexts( packagePaths, corePackagePath ) { +function getPackageContexts( packagePaths, corePackagePath, langContextSuffix ) { // Add path to core package if not included in the package paths. if ( !packagePaths.includes( corePackagePath ) ) { packagePaths = [ ...packagePaths, corePackagePath ]; } const mapEntries = packagePaths - .filter( packagePath => containsContextFile( packagePath ) ) + .filter( packagePath => containsContextFile( packagePath, langContextSuffix ) ) .map( packagePath => { const pathToContext = path.join( packagePath, langContextSuffix ); const packageName = packagePath.split( /[\\/]/ ).pop(); @@ -137,10 +137,10 @@ function collectSourceMessages( { sourceFiles, logger } ) { } /** - * @param {Object} options - * @param {Map.} options.packageContexts A map of language contexts. + * @param {object} options + * @param {Map.} options.packageContexts A map of language contexts. * @param {Array.} options.sourceMessages An array of i18n source messages. - * @returns {Array.} + * @returns {Array.} */ function assertNoMissingContext( { packageContexts, sourceMessages } ) { const errors = []; @@ -162,13 +162,13 @@ function assertNoMissingContext( { packageContexts, sourceMessages } ) { } /** - * @param {Object} options - * @param {Map.} options.packageContexts A map of language contexts. + * @param {object} options + * @param {Map.} options.packageContexts A map of language contexts. * @param {Array.} options.sourceMessages An array of i18n source messages. - * @returns {Array.} + * @returns {Array.} */ function assertAllContextUsed( options ) { - const { packageContexts, sourceMessages, ignoreUnusedCorePackageContexts, corePackagePath } = options; + const { packageContexts, sourceMessages, ignoreUnusedCorePackageContexts, corePackagePath, langContextSuffix } = options; const usedContextMap = new Map(); const errors = []; @@ -207,9 +207,9 @@ function assertAllContextUsed( options ) { } /** - * @param {Object} options - * @param {Map.} options.packageContexts A map of language contexts. - * @returns {Array.} + * @param {object} options + * @param {Map.} options.packageContexts A map of language contexts. + * @returns {Array.} */ function assertNoRepeatedContext( { packageContexts } ) { const errors = []; @@ -229,18 +229,18 @@ function assertNoRepeatedContext( { packageContexts } ) { } function removeExistingPotFiles( translationsDirectory ) { - del.sync( translationsDirectory ); + deleteSync( translationsDirectory ); } /** * Creates a POT file for the given package and POT file content. * The default place is `build/.transifex/[packageName]/en.pot`. * - * @param {Object} options + * @param {object} options * @param {Logger} options.logger - * @param {String} options.packageName - * @param {String} options.translationsDirectory - * @param {String} options.fileContent + * @param {string} options.packageName + * @param {string} options.translationsDirectory + * @param {string} options.fileContent */ function savePotFile( { packageName, fileContent, translationsDirectory, logger } ) { const outputFilePath = path.join( translationsDirectory, packageName, 'en.pot' ); @@ -253,7 +253,7 @@ function savePotFile( { packageName, fileContent, translationsDirectory, logger /** * Creates a POT file header. * - * @returns {String} + * @returns {string} */ function createPotFileHeader() { const year = new Date().getFullYear(); @@ -264,8 +264,8 @@ function createPotFileHeader() { /** * Returns source messages found in the given file with additional data (`filePath` and `packageName`). * - * @param {String} filePath - * @param {String} fileContent + * @param {string} filePath + * @param {string} fileContent * @returns {Array.} */ function getSourceMessagesFromFile( { filePath, fileContent, logger } ) { @@ -291,7 +291,7 @@ function getSourceMessagesFromFile( { filePath, fileContent, logger } ) { * Creates a POT file from the given i18n messages. * * @param {Array.} messages - * @returns {String} + * @returns {string} */ function createPotFileContent( messages ) { return messages.map( message => { @@ -322,28 +322,28 @@ function createPotFileContent( messages ) { } /** - * @param {String} packageDirectory + * @param {string} packageDirectory */ -function containsContextFile( packageDirectory ) { +function containsContextFile( packageDirectory, langContextSuffix ) { return fs.existsSync( path.join( packageDirectory, langContextSuffix ) ); } /** - * @typedef {Object} Message + * @typedef {object} Message * - * @property {String} id - * @property {String} string - * @property {String} filePath - * @property {String} packagePath - * @property {String} context - * @property {String} [plural] + * @property {string} id + * @property {string} string + * @property {string} filePath + * @property {string} packagePath + * @property {string} context + * @property {string} [plural] */ /** - * @typedef {Object} Context + * @typedef {object} Context * - * @property {String} filePath A path to the context file. - * @property {Object} content The context file content - a map of messageId->messageContext records. - * @property {String} packagePath The owner of the context file. - * @property {String} packageName The owner package name. + * @property {string} filePath A path to the context file. + * @property {object} content The context file content - a map of messageId->messageContext records. + * @property {string} packagePath The owner of the context file. + * @property {string} packageName The owner package name. */ diff --git a/packages/ckeditor5-dev-transifex/lib/data/index.js b/packages/ckeditor5-dev-transifex/lib/data/index.js new file mode 100644 index 000000000..c00283406 --- /dev/null +++ b/packages/ckeditor5-dev-transifex/lib/data/index.js @@ -0,0 +1,8 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { default as _languageCodeMap } from './languagecodemap.json' with { type: 'json' }; + +export const languageCodeMap = _languageCodeMap; diff --git a/packages/ckeditor5-dev-transifex/lib/languagecodemap.json b/packages/ckeditor5-dev-transifex/lib/data/languagecodemap.json similarity index 100% rename from packages/ckeditor5-dev-transifex/lib/languagecodemap.json rename to packages/ckeditor5-dev-transifex/lib/data/languagecodemap.json diff --git a/packages/ckeditor5-dev-transifex/lib/download.js b/packages/ckeditor5-dev-transifex/lib/download.js index a9bb21648..1d26b5b50 100644 --- a/packages/ckeditor5-dev-transifex/lib/download.js +++ b/packages/ckeditor5-dev-transifex/lib/download.js @@ -3,18 +3,14 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const fs = require( 'fs-extra' ); -const chalk = require( 'chalk' ); -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); -const { cleanPoFileContent, createDictionaryFromPoFileContent } = require( '@ckeditor/ckeditor5-dev-translations' ); -const transifexService = require( './transifexservice' ); -const { verifyProperties, createLogger } = require( './utils' ); -const languageCodeMap = require( './languagecodemap.json' ); - -const logger = createLogger(); +import path from 'path'; +import fs from 'fs-extra'; +import chalk from 'chalk'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import { cleanPoFileContent, createDictionaryFromPoFileContent } from '@ckeditor/ckeditor5-dev-translations'; +import transifexService from './transifexservice.js'; +import { verifyProperties, createLogger } from './utils.js'; +import { languageCodeMap } from './data/index.js'; /** * Downloads translations from the Transifex for each localizable package. It creates `*.po` files out of the translations and replaces old @@ -22,18 +18,20 @@ const logger = createLogger(); * file is created, containing information about the packages and languages for which the translations could not be downloaded. This file is * then used next time this script is run: it will try to download translations only for packages and languages that failed previously. * - * @param {Object} config - * @param {String} config.organizationName Name of the organization to which the project belongs. - * @param {String} config.projectName Name of the project for downloading the translations. - * @param {String} config.token Token to the Transifex API. - * @param {Map.} config.packages A resource name -> package path map for which translations should be downloaded. + * @param {object} config + * @param {string} config.organizationName Name of the organization to which the project belongs. + * @param {string} config.projectName Name of the project for downloading the translations. + * @param {string} config.token Token to the Transifex API. + * @param {Map.} config.packages A resource name -> package path map for which translations should be downloaded. * The resource name must be the same as the name used in the Transifex service. The package path could be any local path fragment, where * the downloaded translation will be stored. The final path for storing the translations is a combination of the `config.cwd` with the * mentioned package path and the `lang/translations` subdirectory. - * @param {String} config.cwd Current work directory. - * @param {Boolean} [config.simplifyLicenseHeader=false] Whether to skip adding the contribute guide URL in the output `*.po` files. + * @param {string} config.cwd Current work directory. + * @param {boolean} [config.simplifyLicenseHeader=false] Whether to skip adding the contribute guide URL in the output `*.po` files. */ -module.exports = async function downloadTranslations( config ) { +export default async function downloadTranslations( config ) { + const logger = createLogger(); + verifyProperties( config, [ 'organizationName', 'projectName', 'token', 'packages', 'cwd' ] ); transifexService.init( config.token ); @@ -106,7 +104,7 @@ module.exports = async function downloadTranslations( config ) { } else { logger.progress( 'Saved all translations.' ); } -}; +} /** * Saves all valid translations on the filesystem. For each translation entry: @@ -115,11 +113,11 @@ module.exports = async function downloadTranslations( config ) { * (2) Check if the language code should be mapped to another string on the filesystem. * (3) Prepare the translation for storing on the filesystem: remove personal data and add a banner with information how to contribute. * - * @param {Object} config - * @param {String} config.pathToTranslations Path to translations. - * @param {Map.} config.translations The translation map: language code -> translation content. - * @param {Boolean} config.simplifyLicenseHeader Whether to skip adding the contribute guide URL in the output `*.po` files. - * @returns {Number} Number of saved files. + * @param {object} config + * @param {string} config.pathToTranslations Path to translations. + * @param {Map.} config.translations The translation map: language code -> translation content. + * @param {boolean} config.simplifyLicenseHeader Whether to skip adding the contribute guide URL in the output `*.po` files. + * @returns {number} Number of saved files. */ function saveNewTranslations( { pathToTranslations, translations, simplifyLicenseHeader } ) { let savedFiles = 0; @@ -151,13 +149,13 @@ function saveNewTranslations( { pathToTranslations, translations, simplifyLicens * (1) If previous download procedure ended successfully, all translations for all resources will be downloaded. * (2) Otherwise, only packages and their failed translation downloads defined in `.transifex-failed-downloads.json` are taken into account. * - * @param {Object} config - * @param {String} config.cwd Current work directory. - * @param {Array.} config.resources All found resource instances for which translations could be downloaded. - * @param {Array.} config.languages All found language instances in the project. - * @returns {Object} result - * @returns {Boolean} result.isFailedDownloadFileAvailable Indicates whether previous download procedure did not fetch all translations. - * @returns {Array.} result.resourcesToProcess Resource instances and their associated language instances to use during downloading + * @param {object} config + * @param {string} config.cwd Current work directory. + * @param {Array.} config.resources All found resource instances for which translations could be downloaded. + * @param {Array.} config.languages All found language instances in the project. + * @returns {object} result + * @returns {boolean} result.isFailedDownloadFileAvailable Indicates whether previous download procedure did not fetch all translations. + * @returns {Array.} result.resourcesToProcess Resource instances and their associated language instances to use during downloading * the translations. */ function getResourcesToProcess( { cwd, resources, languages } ) { @@ -195,9 +193,9 @@ function getResourcesToProcess( { cwd, resources, languages } ) { /** * Saves all the failed downloads to `.transifex-failed-downloads.json` file. If there are no failures, the file is removed. * - * @param {Object} config - * @param {String} config.cwd Current work directory. - * @param {Array.} config.failedDownloads Collection of all the failed downloads. + * @param {object} config + * @param {string} config.cwd Current work directory. + * @param {Array.} config.failedDownloads Collection of all the failed downloads. */ function updateFailedDownloads( { cwd, failedDownloads } ) { const pathToFailedDownloads = getPathToFailedDownloads( cwd ); @@ -226,8 +224,8 @@ function updateFailedDownloads( { cwd, failedDownloads } ) { /** * Checks if the received data is a translation. * - * @param {String} poFileContent Received data. - * @returns {Boolean} + * @param {string} poFileContent Received data. + * @returns {boolean} */ function isPoFileContainingTranslations( poFileContent ) { const translations = createDictionaryFromPoFileContent( poFileContent ); @@ -239,8 +237,8 @@ function isPoFileContainingTranslations( poFileContent ) { /** * Returns an absolute path to the file containing failed downloads. * - * @param {String} cwd Current working directory. - * @returns {String} + * @param {string} cwd Current working directory. + * @returns {string} */ function getPathToFailedDownloads( cwd ) { return path.join( cwd, '.transifex-failed-downloads.json' ); diff --git a/packages/ckeditor5-dev-transifex/lib/gettoken.js b/packages/ckeditor5-dev-transifex/lib/gettoken.js index 3211a4b81..6db12f86d 100644 --- a/packages/ckeditor5-dev-transifex/lib/gettoken.js +++ b/packages/ckeditor5-dev-transifex/lib/gettoken.js @@ -3,16 +3,14 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const inquirer = require( 'inquirer' ); +import inquirer from 'inquirer'; /** * Takes username and password from prompt and returns promise that resolves with object that contains them. * - * @returns {Promise.} + * @returns {Promise.} */ -module.exports = async function getToken() { +export default async function getToken() { const { token } = await inquirer.prompt( [ { type: 'password', message: 'Provide the Transifex token (generate it here: https://www.transifex.com/user/settings/api/):', @@ -20,4 +18,4 @@ module.exports = async function getToken() { } ] ); return token; -}; +} diff --git a/packages/ckeditor5-dev-transifex/lib/index.js b/packages/ckeditor5-dev-transifex/lib/index.js index 1cef95915..b912148a8 100644 --- a/packages/ckeditor5-dev-transifex/lib/index.js +++ b/packages/ckeditor5-dev-transifex/lib/index.js @@ -3,20 +3,12 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import * as _transifexUtils from './utils.js'; -const createPotFiles = require( './createpotfiles' ); -const uploadPotFiles = require( './upload' ); -const downloadTranslations = require( './download' ); -const getToken = require( './gettoken' ); -const transifexService = require( './transifexservice' ); -const transifexUtils = require( './utils' ); +export const transifexUtils = _transifexUtils; -module.exports = { - createPotFiles, - uploadPotFiles, - downloadTranslations, - getToken, - transifexService, - transifexUtils -}; +export { default as createPotFiles } from './createpotfiles.js'; +export { default as uploadPotFiles } from './upload.js'; +export { default as downloadTranslations } from './download.js'; +export { default as getToken } from './gettoken.js'; +export { default as transifexService } from './transifexservice.js'; diff --git a/packages/ckeditor5-dev-transifex/lib/transifexservice.js b/packages/ckeditor5-dev-transifex/lib/transifexservice.js index d8563045d..e74eeb70b 100644 --- a/packages/ckeditor5-dev-transifex/lib/transifexservice.js +++ b/packages/ckeditor5-dev-transifex/lib/transifexservice.js @@ -3,8 +3,7 @@ * For licensing, see LICENSE.md. */ -const { transifexApi } = require( '@transifex/api' ); -const fetch = require( 'node-fetch' ); +import { transifexApi } from '@transifex/api'; const MAX_REQUEST_ATTEMPTS = 10; const REQUEST_RETRY_TIMEOUT = 3000; // In milliseconds. @@ -18,7 +17,7 @@ const REQUEST_START_OFFSET_TIMEOUT = 100; * * @see https://docs.transifex.com/api-3-0/introduction-to-api-3-0 for API documentation. */ -module.exports = { +export default { init, getProjectData, getTranslations, @@ -34,7 +33,7 @@ module.exports = { /** * Configures the API token for Transifex service if it has not been set yet. * - * @param {String} token Token for the Transifex API. + * @param {string} token Token for the Transifex API. */ function init( token ) { if ( !transifexApi.auth ) { @@ -45,10 +44,10 @@ function init( token ) { /** * Creates a new resource on Transifex. * - * @param {Object} options - * @param {String} options.organizationName The name of the organization to which the project belongs. - * @param {String} options.projectName The name of the project for creating the resource. - * @param {String} options.resourceName The name of the resource to create. + * @param {object} options + * @param {string} options.organizationName The name of the organization to which the project belongs. + * @param {string} options.projectName The name of the project for creating the resource. + * @param {string} options.resourceName The name of the resource to create. * @returns {Promise} */ async function createResource( options ) { @@ -78,12 +77,12 @@ async function createResource( options ) { /** * Uploads a new translations source for the specified resource (package). * - * @param {Object} options - * @param {String} options.organizationName The name of the organization to which the project belongs. - * @param {String} options.projectName The name of the project for uploading the translations entries. - * @param {String} options.resourceName The The name of resource. - * @param {String} options.content A content of the `*.po` file containing source for translations. - * @returns {Promise.} + * @param {object} options + * @param {string} options.organizationName The name of the organization to which the project belongs. + * @param {string} options.projectName The name of the project for uploading the translations entries. + * @param {string} options.resourceName The The name of resource. + * @param {string} options.content A content of the `*.po` file containing source for translations. + * @returns {Promise.} */ async function createSourceFile( options ) { const { organizationName, projectName, resourceName, content } = options; @@ -111,8 +110,8 @@ async function createSourceFile( options ) { * Resolves a promise containing an object with a summary of processing the uploaded source * file created by the Transifex service if the upload task is completed. * - * @param {String} uploadId - * @param {Number} [numberOfAttempts=1] A number containing a current attempt. + * @param {string} uploadId + * @param {number} [numberOfAttempts=1] A number containing a current attempt. * @returns {Promise} */ async function getResourceUploadDetails( uploadId, numberOfAttempts = 1 ) { @@ -141,12 +140,12 @@ async function getResourceUploadDetails( uploadId, numberOfAttempts = 1 ) { /** * Retrieves all the resources and languages associated with the requested project within given organization from the Transifex service. * - * @param {String} organizationName Name of the organization to which the project belongs. - * @param {String} projectName Name of the project for downloading the translations. - * @param {Array.} localizablePackageNames Names of all packages for which translations should be downloaded. - * @returns {Promise.} result - * @returns {Array.} result.resources All found resource instances for which translations could be downloaded. - * @returns {Array.} result.languages All found language instances in the project. + * @param {string} organizationName Name of the organization to which the project belongs. + * @param {string} projectName Name of the project for downloading the translations. + * @param {Array.} localizablePackageNames Names of all packages for which translations should be downloaded. + * @returns {Promise.} result + * @returns {Array.} result.resources All found resource instances for which translations could be downloaded. + * @returns {Array.} result.languages All found language instances in the project. */ async function getProjectData( organizationName, projectName, localizablePackageNames ) { const organization = await transifexApi.Organization.get( { slug: organizationName } ); @@ -188,11 +187,11 @@ async function getProjectData( organizationName, projectName, localizablePackage * The download procedure is not interrupted when any request has failed for any reason, but continues until the end for each language. * Failed requests and all fetched translations are collected and returned to the caller for further processing. * - * @param {Object} resource The resource instance for which translations should be downloaded. - * @param {Array.} languages An array of all the language instances found in the project. - * @returns {Promise.} result - * @returns {Map.} result.translations The translation map: language code -> translation content. - * @returns {Array.} result.failedDownloads Collection of all the failed downloads. + * @param {object} resource The resource instance for which translations should be downloaded. + * @param {Array.} languages An array of all the language instances found in the project. + * @returns {Promise.} result + * @returns {Map.} result.translations The translation map: language code -> translation content. + * @returns {Array.} result.failedDownloads Collection of all the failed downloads. */ async function getTranslations( resource, languages ) { const downloadRequests = await Promise @@ -242,9 +241,9 @@ async function getTranslations( resource, languages ) { * Fetches all the translations for the specified resource and language. The returned array contains translation items (objects) with * attributes and relationships to other Transifex entities. * - * @param {String} resourceId The resource id for which translation should be downloaded. - * @param {String} languageId The language id for which translation should be downloaded. - * @returns {Promise.>} + * @param {string} resourceId The resource id for which translation should be downloaded. + * @param {string} languageId The language id for which translation should be downloaded. + * @returns {Promise.>} */ async function getResourceTranslations( resourceId, languageId ) { const translations = transifexApi.ResourceTranslation @@ -275,10 +274,10 @@ async function getResourceTranslations( resourceId, languageId ) { /** * Creates the download request for the given resource and the language. * - * @param {Object} resource The resource instance for which translation should be downloaded. - * @param {Object} language The language instance for which translation should be downloaded. - * @param {Number} [numberOfAttempts=1] Current number of request attempt. - * @returns {Promise.} + * @param {object} resource The resource instance for which translation should be downloaded. + * @param {object} language The language instance for which translation should be downloaded. + * @param {number} [numberOfAttempts=1] Current number of request attempt. + * @returns {Promise.} */ function createDownloadRequest( resource, language, numberOfAttempts = 1 ) { const attributes = { @@ -321,24 +320,25 @@ function createDownloadRequest( resource, language, numberOfAttempts = 1 ) { * attempt. There are three possible cases that are handled during downloading a file: * * (1) According to the Transifex API v3.0, when the requested file is ready for download, the Transifex service returns HTTP code 303, - * which is the redirection to the new location, where the file is available. By default, `node-fetch` follows redirections so the requested + * which is the redirection to the new location, where the file is available. By default, `fetch` follows redirections so the requested * file is downloaded automatically. * (2) If the requested file is not ready yet, but the response status from the Transifex service was successful and the number of retries * has not reached the limit yet, the request is queued and retried after the REQUEST_RETRY_TIMEOUT timeout. * (3) Otherwise, there is either a problem with downloading a file (the request has failed) or the number of retries has reached the limit, * so rejected promise is returned. * - * @param {Object} downloadRequest Data that defines the requested file. - * @param {String} downloadRequest.url URL where generated PO file will be available to download. - * @param {String} downloadRequest.resourceName Package name for which the URL is generated. - * @param {String} downloadRequest.languageCode Language code for which the URL is generated. - * @param {Number} [numberOfAttempts=1] Current number of download attempt. - * @returns {Promise.>} The 2-element array: the language code at index 0 and the translation content at index 1. + * @param {object} downloadRequest Data that defines the requested file. + * @param {string} downloadRequest.url URL where generated PO file will be available to download. + * @param {string} downloadRequest.resourceName Package name for which the URL is generated. + * @param {string} downloadRequest.languageCode Language code for which the URL is generated. + * @param {number} [numberOfAttempts=1] Current number of download attempt. + * @returns {Promise.>} The 2-element array: the language code at index 0 and the translation content at index 1. */ async function downloadFile( downloadRequest, numberOfAttempts = 1 ) { const { url, resourceName, languageCode } = downloadRequest; const response = await fetch( url, { + method: 'GET', headers: { ...transifexApi.auth() } @@ -374,8 +374,8 @@ async function downloadFile( downloadRequest, numberOfAttempts = 1 ) { /** * Retrieves the resource name (the package name) from the resource instance. * - * @param {Object} resource Resource instance. - * @returns {String} + * @param {object} resource Resource instance. + * @returns {string} */ function getResourceName( resource ) { return resource.attributes.slug; @@ -384,8 +384,8 @@ function getResourceName( resource ) { /** * Retrieves the language code from the language instance. * - * @param {Object} language Language instance. - * @returns {String} + * @param {object} language Language instance. + * @returns {string} */ function getLanguageCode( language ) { return language.attributes.code; @@ -395,7 +395,7 @@ function getLanguageCode( language ) { * Creates an artificial Transifex language instance for the source language, which is English. The language instance for the source strings * is needed, because Transifex service has two dedicated API resources: one for the translations and another one for the source strings. * - * @returns {Object} + * @returns {object} */ function createSourceLanguage() { return { @@ -408,8 +408,8 @@ function createSourceLanguage() { /** * Checks if the language instance is the source language, which is English. * - * @param {Object} language Language instance. - * @returns {Boolean} + * @param {object} language Language instance. + * @returns {boolean} */ function isSourceLanguage( language ) { return getLanguageCode( language ) === 'en'; @@ -418,7 +418,7 @@ function isSourceLanguage( language ) { /** * Returns results from each rejected promise, which are returned from the `Promise.allSettled()` method. * - * @param {Array.} results Collection of objects that each describes the outcome of each promise. + * @param {Array.} results Collection of objects that each describes the outcome of each promise. * @returns {Array.<*>} */ function getFailedResults( results ) { @@ -430,7 +430,7 @@ function getFailedResults( results ) { /** * Returns results from each fulfilled promise, which are returned from the `Promise.allSettled()` method. * - * @param {Array.} results Collection of objects that each describes the outcome of each promise. + * @param {Array.} results Collection of objects that each describes the outcome of each promise. * @returns {Array.<*>} */ function getSuccessfulResults( results ) { @@ -442,7 +442,7 @@ function getSuccessfulResults( results ) { /** * Simple promisified timeout that resolves after defined number of milliseconds. * - * @param {Number} numberOfMilliseconds Number of milliseconds after which the promise will be resolved. + * @param {number} numberOfMilliseconds Number of milliseconds after which the promise will be resolved. * @returns {Promise} */ function wait( numberOfMilliseconds ) { diff --git a/packages/ckeditor5-dev-transifex/lib/upload.js b/packages/ckeditor5-dev-transifex/lib/upload.js index 20aab16ba..6deb7c143 100644 --- a/packages/ckeditor5-dev-transifex/lib/upload.js +++ b/packages/ckeditor5-dev-transifex/lib/upload.js @@ -3,20 +3,16 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const fs = require( 'fs/promises' ); -const path = require( 'path' ); -const Table = require( 'cli-table' ); -const chalk = require( 'chalk' ); -const transifexService = require( './transifexservice' ); -const { verifyProperties, createLogger } = require( './utils' ); -const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); +import fs from 'fs/promises'; +import path from 'path'; +import Table from 'cli-table'; +import chalk from 'chalk'; +import transifexService from './transifexservice.js'; +import { verifyProperties, createLogger } from './utils.js'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; const RESOURCE_REGEXP = /r:(?[a-z0-9_-]+)$/i; -const TRANSIFEX_RESOURCE_ERRORS = {}; - /** * Uploads translations to the Transifex. * @@ -30,15 +26,17 @@ const TRANSIFEX_RESOURCE_ERRORS = {}; * The Transifex API may end with an error at any stage. In such a case, the resource is not processed anymore. * It is saved to a file (`.transifex-failed-uploads.json`). Rerunning the script will process only packages specified in the file. * - * @param {Object} config - * @param {String} config.token Token to the Transifex API. - * @param {String} config.organizationName Name of the organization to which the project belongs. - * @param {String} config.projectName Name of the project for downloading the translations. - * @param {String} config.cwd Current work directory. - * @param {Map.} config.packages A resource name -> package path map for which translations should be uploaded. + * @param {object} config + * @param {string} config.token Token to the Transifex API. + * @param {string} config.organizationName Name of the organization to which the project belongs. + * @param {string} config.projectName Name of the project for downloading the translations. + * @param {string} config.cwd Current work directory. + * @param {Map.} config.packages A resource name -> package path map for which translations should be uploaded. * @returns {Promise} */ -module.exports = async function upload( config ) { +export default async function upload( config ) { + const TRANSIFEX_RESOURCE_ERRORS = {}; + verifyProperties( config, [ 'token', 'organizationName', 'projectName', 'cwd', 'packages' ] ); const logger = createLogger(); @@ -53,7 +51,9 @@ module.exports = async function upload( config ) { logger.warning( 'Found the file containing a list of packages that failed during the last script execution.' ); logger.warning( 'The script will process only packages listed in the file instead of all passed as "config.packages".' ); - failedPackages = Object.keys( require( pathToFailedUploads ) ); + failedPackages = Object.keys( + ( await import( pathToFailedUploads ) ).default + ); } logger.progress( 'Fetching project information...' ); @@ -104,11 +104,11 @@ module.exports = async function upload( config ) { // For new packages, before uploading their translations, we need to create a dedicated resource. if ( isNew ) { await transifexService.createResource( { organizationName, projectName, resourceName } ) - .catch( errorHandlerFactory( resourceName, spinner ) ); + .catch( errorHandlerFactory( TRANSIFEX_RESOURCE_ERRORS, resourceName, spinner ) ); } // Abort if creating the resource ended with an error. - if ( hasError( resourceName ) ) { + if ( hasError( TRANSIFEX_RESOURCE_ERRORS, resourceName ) ) { continue; } @@ -119,7 +119,7 @@ module.exports = async function upload( config ) { uploadIds.push( { resourceName, uuid } ); spinner.finish(); } ) - .catch( errorHandlerFactory( resourceName, spinner ) ); + .catch( errorHandlerFactory( TRANSIFEX_RESOURCE_ERRORS, resourceName, spinner ) ); } // An empty line for making a space between list of resources and the new process info. @@ -133,7 +133,7 @@ module.exports = async function upload( config ) { const uploadDetails = uploadIds.map( async ( { resourceName, uuid } ) => { return transifexService.getResourceUploadDetails( uuid ) - .catch( errorHandlerFactory( resourceName ) ); + .catch( errorHandlerFactory( TRANSIFEX_RESOURCE_ERRORS, resourceName ) ); } ); const summary = ( await Promise.all( uploadDetails ) ) @@ -157,7 +157,7 @@ module.exports = async function upload( config ) { logger.info( table.toString() ); } - if ( hasError() ) { + if ( hasError( TRANSIFEX_RESOURCE_ERRORS ) ) { // An empty line for making a space between steps and the warning message. logger.info( '' ); logger.warning( 'Not all translations were uploaded due to errors in Transifex API.' ); @@ -171,7 +171,7 @@ module.exports = async function upload( config ) { else if ( isFailedUploadFileAvailable ) { await fs.unlink( pathToFailedUploads ); } -}; +} /** * Returns a factory function that process a response from Transifex and prepares a single resource @@ -266,10 +266,11 @@ function formatTableRow() { * * If the `packageName` is not specified, returns `true` if any error occurs. * - * @param {String|null} [packageName=null] - * @returns {Boolean} + * @param {object} [TRANSIFEX_RESOURCE_ERRORS] + * @param {string|null} [packageName=null] + * @returns {boolean} */ -function hasError( packageName = null ) { +function hasError( TRANSIFEX_RESOURCE_ERRORS, packageName = null ) { if ( !packageName ) { return Boolean( Object.keys( TRANSIFEX_RESOURCE_ERRORS ).length ); } @@ -280,11 +281,12 @@ function hasError( packageName = null ) { /** * Creates a callback that stores errors from Transifex for the given `packageName`. * - * @param {String} packageName + * @param {object} [TRANSIFEX_RESOURCE_ERRORS] + * @param {string} packageName * @param {CKEditor5Spinner|null} [spinner=null] * @returns {Function} */ -function errorHandlerFactory( packageName, spinner ) { +function errorHandlerFactory( TRANSIFEX_RESOURCE_ERRORS, packageName, spinner ) { return errorResponse => { if ( spinner ) { spinner.finish( { emoji: '❌' } ); @@ -297,8 +299,8 @@ function errorHandlerFactory( packageName, spinner ) { } /** - * @param {String} pathToFile - * @returns {Promise.} + * @param {string} pathToFile + * @returns {Promise.} */ function isFile( pathToFile ) { return fs.lstat( pathToFile ) diff --git a/packages/ckeditor5-dev-transifex/lib/utils.js b/packages/ckeditor5-dev-transifex/lib/utils.js index 530a2ecb4..939ce35c8 100644 --- a/packages/ckeditor5-dev-transifex/lib/utils.js +++ b/packages/ckeditor5-dev-transifex/lib/utils.js @@ -3,51 +3,45 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import chalk from 'chalk'; +import { logger as loggerFactory } from '@ckeditor/ckeditor5-dev-utils'; -const chalk = require( 'chalk' ); -const { logger: loggerFactory } = require( '@ckeditor/ckeditor5-dev-utils' ); - -const utils = { - /** - * Checks whether specified `properties` are specified in the `objectToCheck` object. - * - * Throws an error if any property is missing. - * - * @param {Object} objectToCheck - * @param {Array.} properties - */ - verifyProperties( objectToCheck, properties ) { - const nonExistingProperties = properties.filter( property => objectToCheck[ property ] === undefined ); - - if ( nonExistingProperties.length ) { - throw new Error( `The specified object misses the following properties: ${ nonExistingProperties.join( ', ' ) }.` ); - } - }, - - /** - * Creates logger instance. - * - * @returns {Object} logger - * @returns {Function} logger.progress - * @returns {Function} logger.info - * @returns {Function} logger.warning - * @returns {Function} logger.error - */ - createLogger() { - const logger = loggerFactory(); +/** + * Checks whether specified `properties` are specified in the `objectToCheck` object. + * + * Throws an error if any property is missing. + * + * @param {object} objectToCheck + * @param {Array.} properties + */ +export function verifyProperties( objectToCheck, properties ) { + const nonExistingProperties = properties.filter( property => objectToCheck[ property ] === undefined ); - return { - progress( message ) { - if ( !message ) { - this.info( '' ); - } else { - this.info( '\n📍 ' + chalk.cyan( message ) ); - } - }, - ...logger - }; + if ( nonExistingProperties.length ) { + throw new Error( `The specified object misses the following properties: ${ nonExistingProperties.join( ', ' ) }.` ); } -}; +} + +/** + * Creates logger instance. + * + * @returns {object} logger + * @returns {Function} logger.progress + * @returns {Function} logger.info + * @returns {Function} logger.warning + * @returns {Function} logger.error + */ +export function createLogger() { + const logger = loggerFactory(); -module.exports = utils; + return { + progress( message ) { + if ( !message ) { + this.info( '' ); + } else { + this.info( '\n📍 ' + chalk.cyan( message ) ); + } + }, + ...logger + }; +} diff --git a/packages/ckeditor5-dev-transifex/package.json b/packages/ckeditor5-dev-transifex/package.json index 7d68ce4b8..e8aec5b56 100644 --- a/packages/ckeditor5-dev-transifex/package.json +++ b/packages/ckeditor5-dev-transifex/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-transifex", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "Used to download and upload translations using the Transifex service.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -17,29 +17,25 @@ "npm": ">=5.7.1" }, "main": "lib/index.js", + "type": "module", "files": [ "lib" ], "dependencies": { - "fs-extra": "^11.2.0", - "del": "^5.1.0", - "@ckeditor/ckeditor5-dev-utils": "^43.0.0", - "@ckeditor/ckeditor5-dev-translations": "^43.0.0", - "chalk": "^4.0.0", - "inquirer": "^7.1.0", - "@transifex/api": "^4.2.1", - "node-fetch": "^2.6.7", + "fs-extra": "^11.0.0", + "del": "^7.0.0", + "@ckeditor/ckeditor5-dev-utils": "^44.0.0-alpha.5", + "@ckeditor/ckeditor5-dev-translations": "^44.0.0-alpha.5", + "chalk": "^5.0.0", + "inquirer": "^11.0.0", + "@transifex/api": "^7.0.0", "cli-table": "^0.3.1" }, "devDependencies": { - "chai": "^4.2.0", - "mocha": "^7.1.2", - "mockery": "^2.1.0", - "proxyquire": "^2.1.3", - "sinon": "^9.2.4" + "vitest": "^2.0.5" }, "scripts": { - "test": "mocha './tests/**/*.js' --timeout 10000", - "coverage": "nyc --reporter=lcov --reporter=text-summary yarn run test" + "test": "vitest run --config vitest.config.js", + "coverage": "vitest run --config vitest.config.js --coverage" } } diff --git a/packages/ckeditor5-dev-transifex/tests/createpotfiles.js b/packages/ckeditor5-dev-transifex/tests/createpotfiles.js index b5c264a09..dec22caae 100644 --- a/packages/ckeditor5-dev-transifex/tests/createpotfiles.js +++ b/packages/ckeditor5-dev-transifex/tests/createpotfiles.js @@ -3,52 +3,33 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import createPotFiles from '../lib/createpotfiles.js'; -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); -const { posix } = require( 'path' ); -const { expect } = require( 'chai' ); +import { findMessages } from '@ckeditor/ckeditor5-dev-translations'; +import { verifyProperties } from '../lib/utils.js'; +import { deleteSync } from 'del'; +import fs from 'fs-extra'; +import path from 'path'; + +vi.mock( '../lib/utils.js' ); +vi.mock( '@ckeditor/ckeditor5-dev-translations' ); +vi.mock( 'del' ); +vi.mock( 'fs-extra' ); +vi.mock( 'path' ); describe( 'dev-transifex/createPotFiles()', () => { - let stubs; - let createPotFiles; + let loggerMocks; beforeEach( () => { - stubs = { - logger: { - info: sinon.stub(), - warning: sinon.stub(), - error: sinon.stub() - }, - - translations: { - findMessages: sinon.stub() - }, - - del: { - sync: sinon.stub() - }, - - fs: { - readFileSync: sinon.stub(), - outputFileSync: sinon.stub(), - existsSync: sinon.stub() - } + loggerMocks = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn() }; - createPotFiles = proxyquire( '../lib/createpotfiles', { - 'del': stubs.del, - 'fs-extra': stubs.fs, - '@ckeditor/ckeditor5-dev-translations': { - findMessages: stubs.translations.findMessages - }, - 'path': posix - } ); - } ); - - afterEach( () => { - sinon.restore(); + vi.mocked( verifyProperties ).mockImplementation( vi.fn() ); + vi.mocked( path.join ).mockImplementation( ( ...args ) => args.join( '/' ) ); } ); it( 'should not create any POT file if no package is passed', () => { @@ -57,207 +38,403 @@ describe( 'dev-transifex/createPotFiles()', () => { packagePaths: [], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.notCalled( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 0 ); } ); it( 'should delete the build directory before creating POT files', () => { - createFakeContextFile( 'packages/ckeditor5-foo/lang/contexts.json', { foo_id: 'foo_context' } ); - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ { string: 'foo', id: 'foo_id' } ] ); - createPotFiles( { sourceFiles: [ 'packages/ckeditor5-foo/src/foo.js' ], packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.calledOnce( stubs.del.sync ); - sinon.assert.calledWithExactly( stubs.del.sync, '/cwd/build/.transifex' ); - sinon.assert.callOrder( stubs.del.sync, stubs.fs.outputFileSync ); + expect( vi.mocked( deleteSync ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( deleteSync ) ).toHaveBeenCalledWith( '/cwd/build/.transifex' ); } ); it( 'should create a POT file entry for one message with a corresponding context', () => { - createFakeContextFile( 'packages/ckeditor5-foo/lang/contexts.json', { foo_id: 'foo_context' } ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); + + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); + } ); createPotFiles( { sourceFiles: [ 'packages/ckeditor5-foo/src/foo.js' ], packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.calledOnce( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync, - '/cwd/build/.transifex/ckeditor5-foo/en.pot', - `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved. + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); -msgctxt "foo_context" -msgid "foo_id" -msgstr "foo" -` + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); + + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledWith( + '/cwd/build/.transifex/ckeditor5-foo/en.pot', + [ + `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved.`, + '', + 'msgctxt "foo_context"', + 'msgid "foo_id"', + 'msgstr "foo"', + '' + ].join( '\n' ) ); } ); it( 'should warn if the message context is missing', () => { - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); + + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); + + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); + } ); createPotFiles( { sourceFiles: [ 'packages/ckeditor5-foo/src/foo.js' ], packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.calledOnce( stubs.logger.error ); - sinon.assert.calledWithExactly( - stubs.logger.error, + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); + + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); + + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); + + expect( loggerMocks.error ).toHaveBeenCalledTimes( 1 ); + expect( loggerMocks.error ).toHaveBeenCalledWith( 'Context for the message id is missing (\'foo_id\' from packages/ckeditor5-foo/src/foo.js).' ); - sinon.assert.notCalled( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 0 ); // Mark the process as failed in case of the error. - expect( process.exitCode ).to.equal( 1 ); + expect( process.exitCode ).toEqual( 1 ); } ); it( 'should create a POT file entry for every defined package', () => { - createFakeContextFile( 'packages/ckeditor5-foo/lang/contexts.json', { foo_id: 'foo_context' } ); - createFakeContextFile( 'packages/ckeditor5-bar/lang/contexts.json', { bar_id: 'bar_context' } ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'bar_id': 'bar_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-bar/src/bar.js_content' ); - createFakeSourceFileWithMessages( 'packages/ckeditor5-bar/src/bar.js', [ - { string: 'bar', id: 'bar_id' } - ] ); + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); + } ); + + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'bar', id: 'bar_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); + } ); createPotFiles( { sourceFiles: [ 'packages/ckeditor5-foo/src/foo.js', 'packages/ckeditor5-bar/src/bar.js' ], packagePaths: [ 'packages/ckeditor5-foo', 'packages/ckeditor5-bar' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.calledTwice( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-bar/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 3, 'packages/ckeditor5-core/lang/contexts.json' + ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync, - '/cwd/build/.transifex/ckeditor5-foo/en.pot', - `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved. + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 4 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-bar/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 3, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 4, 'packages/ckeditor5-bar/src/bar.js', 'utf-8' + ); -msgctxt "foo_context" -msgid "foo_id" -msgstr "foo" -` ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( findMessages ) ).toHaveBeenNthCalledWith( + 1, + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); + expect( vi.mocked( findMessages ) ).toHaveBeenNthCalledWith( + 2, + 'packages/ckeditor5-bar/src/bar.js_content', + 'packages/ckeditor5-bar/src/bar.js', + expect.any( Function ), + expect.any( Function ) + ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync, + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenNthCalledWith( + 1, + '/cwd/build/.transifex/ckeditor5-foo/en.pot', + [ + `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved.`, + '', + 'msgctxt "foo_context"', + 'msgid "foo_id"', + 'msgstr "foo"', + '' + ].join( '\n' ) + ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenNthCalledWith( + 2, '/cwd/build/.transifex/ckeditor5-bar/en.pot', - `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved. - -msgctxt "bar_context" -msgid "bar_id" -msgstr "bar" -` + [ + `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved.`, + '', + 'msgctxt "bar_context"', + 'msgid "bar_id"', + 'msgstr "bar"', + '' + ].join( '\n' ) ); } ); it( 'should create one POT file entry from multiple files in the same package', () => { - createFakeContextFile( 'packages/ckeditor5-foo/lang/contexts.json', { - foo_id: 'foo_context', - bar_id: 'bar_context' + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); + + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context', 'bar_id': 'bar_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/bar.js_content' ); + + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); } ); - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'bar', id: 'bar_id' } + ]; - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/bar.js', [ - { string: 'bar', id: 'bar_id' } - ] ); + messages.forEach( message => onFoundMessage( message ) ); + } ); createPotFiles( { sourceFiles: [ 'packages/ckeditor5-foo/src/foo.js', 'packages/ckeditor5-foo/src/bar.js' ], packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.calledOnce( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync, - '/cwd/build/.transifex/ckeditor5-foo/en.pot', - `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved. + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 3, 'packages/ckeditor5-foo/src/bar.js', 'utf-8' + ); -msgctxt "foo_context" -msgid "foo_id" -msgstr "foo" + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( findMessages ) ).toHaveBeenNthCalledWith( + 1, + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); + expect( vi.mocked( findMessages ) ).toHaveBeenNthCalledWith( + 2, + 'packages/ckeditor5-foo/src/bar.js_content', + 'packages/ckeditor5-foo/src/bar.js', + expect.any( Function ), + expect.any( Function ) + ); -msgctxt "bar_context" -msgid "bar_id" -msgstr "bar" -` + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledWith( + '/cwd/build/.transifex/ckeditor5-foo/en.pot', + [ + `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved.`, + '', + 'msgctxt "foo_context"', + 'msgid "foo_id"', + 'msgstr "foo"', + '', + 'msgctxt "bar_context"', + 'msgid "bar_id"', + 'msgstr "bar"', + '' + ].join( '\n' ) ); } ); it( 'should create a POT entry filled with plural forms for message that contains has defined plural forms', () => { - createFakeContextFile( 'packages/ckeditor5-foo/lang/contexts.json', { - foo_id: 'foo_context' - } ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); + + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); + + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id', plural: 'foo_plural' } + ]; - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id', plural: 'foo_plural' } - ] ); + messages.forEach( message => onFoundMessage( message ) ); + } ); createPotFiles( { sourceFiles: [ 'packages/ckeditor5-foo/src/foo.js' ], packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.calledOnce( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync, - '/cwd/build/.transifex/ckeditor5-foo/en.pot', - `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved. + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); -msgctxt "foo_context" -msgid "foo_id" -msgid_plural "foo_plural" -msgstr[0] "foo" -msgstr[1] "foo_plural" -` + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); + + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledWith( + '/cwd/build/.transifex/ckeditor5-foo/en.pot', + [ + `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved.`, + '', + 'msgctxt "foo_context"', + 'msgid "foo_id"', + 'msgid_plural "foo_plural"', + 'msgstr[0] "foo"', + 'msgstr[1] "foo_plural"', + '' + ].join( '\n' ) ); } ); it( 'should load the core context file once and use its contexts', () => { - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); - createFakeContextFile( 'packages/ckeditor5-core/lang/contexts.json', { - foo_id: 'foo_context' + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); } ); createPotFiles( { @@ -265,32 +442,62 @@ msgstr[1] "foo_plural" packagePaths: [ 'packages/ckeditor5-foo', 'packages/ckeditor5-core' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.notCalled( stubs.logger.error ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); - sinon.assert.calledOnce( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-core/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync, - '/cwd/build/.transifex/ckeditor5-core/en.pot', - `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved. + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); -msgctxt "foo_context" -msgid "foo_id" -msgstr "foo" -` + expect( loggerMocks.error ).toHaveBeenCalledTimes( 0 ); + + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledWith( + '/cwd/build/.transifex/ckeditor5-core/en.pot', + [ + `# Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved.`, + '', + 'msgctxt "foo_context"', + 'msgid "foo_id"', + 'msgstr "foo"', + '' + ].join( '\n' ) ); } ); it( 'should not create a POT file for the context file if that was not added to the list of packages', () => { - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); - createFakeContextFile( 'packages/ckeditor5-core/lang/contexts.json', { - foo_id: 'foo_context' + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); + + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); } ); createPotFiles( { @@ -298,71 +505,161 @@ msgstr "foo" packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.notCalled( stubs.logger.error ); - sinon.assert.notCalled( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); + + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-core/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); + + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); + + expect( loggerMocks.error ).toHaveBeenCalledTimes( 0 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 0 ); } ); it( 'should log an error if the file contains a message that cannot be parsed', () => { - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [], [ 'parse_error' ] ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); + + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); + + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage, onErrorFound ) => { + const errors = [ + 'parse_error' + ]; + + errors.forEach( error => onErrorFound( error ) ); + } ); createPotFiles( { sourceFiles: [ 'packages/ckeditor5-foo/src/foo.js' ], packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.calledOnce( stubs.logger.error ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); - sinon.assert.calledWithExactly( - stubs.logger.error, - 'parse_error' + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js', 'utf-8' ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); + + expect( loggerMocks.error ).toHaveBeenCalledTimes( 1 ); + expect( loggerMocks.error ).toHaveBeenCalledWith( 'parse_error' ); + // Mark the process as failed in case of the error. - expect( process.exitCode ).to.equal( 1 ); + expect( process.exitCode ).toEqual( 1 ); } ); it( 'should log an error if two context files contain contexts the same id', () => { - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context1' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context2' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); - createFakeContextFile( 'packages/ckeditor5-foo/lang/contexts.json', { foo_id: 'foo_context1' } ); - createFakeContextFile( 'packages/ckeditor5-core/lang/contexts.json', { foo_id: 'foo_context2' } ); + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); + } ); createPotFiles( { sourceFiles: [ 'packages/ckeditor5-foo/src/foo.js' ], packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.calledOnce( stubs.logger.error ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); + + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 3, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); - sinon.assert.calledWithExactly( - stubs.logger.error, + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); + + expect( loggerMocks.error ).toHaveBeenCalledTimes( 1 ); + expect( loggerMocks.error ).toHaveBeenCalledWith( 'Context is duplicated for the id: \'foo_id\' in ' + 'packages/ckeditor5-core/lang/contexts.json and packages/ckeditor5-foo/lang/contexts.json.' ); // Mark the process as failed in case of the error. - expect( process.exitCode ).to.equal( 1 ); + expect( process.exitCode ).toEqual( 1 ); } ); it( 'should log an error if a context is unused', () => { - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); + + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context', 'bar_id': 'foo_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); - createFakeContextFile( 'packages/ckeditor5-foo/lang/contexts.json', { - foo_id: 'foo_context', - bar_id: 'foo_context' + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); } ); createPotFiles( { @@ -370,13 +667,35 @@ msgstr "foo" packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger + logger: loggerMocks } ); - sinon.assert.calledOnce( stubs.logger.error ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); + + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); + + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); - sinon.assert.calledWithExactly( - stubs.logger.error, + expect( loggerMocks.error ).toHaveBeenCalledTimes( 1 ); + expect( loggerMocks.error ).toHaveBeenCalledWith( 'Unused context: \'bar_id\' in ckeditor5-foo/lang/contexts.json' ); @@ -385,28 +704,35 @@ msgstr "foo" } ); it( 'should fail with an error describing missing properties if the required were not passed to the function', () => { + vi.mocked( verifyProperties ).mockImplementationOnce( ( options, requiredProperties ) => { + throw new Error( `The specified object misses the following properties: ${ requiredProperties.join( ', ' ) }.` ); + } ); + try { createPotFiles( {} ); + + throw new Error( 'Expected to throw.' ); } catch ( err ) { - expect( err.message ).to.equal( + expect( err.message ).toEqual( 'The specified object misses the following properties: sourceFiles, packagePaths, corePackagePath, translationsDirectory.' ); } } ); it( 'should not log an error if a context from the core package is unused when ignoreUnusedCorePackageContexts=true', () => { - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); - createFakeContextFile( 'packages/ckeditor5-foo/lang/contexts.json', { - foo_id: 'foo_context', - bar_id: 'foo_context' - } ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context', 'bar_id': 'foo_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'custom_id': 'foo_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); + + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; - // This context is not used anywhere and the test would fail if the `ignoreUnusedCorePackageContexts` flag is set to `false`. - createFakeContextFile( 'packages/ckeditor5-core/lang/contexts.json', { - custom_id: 'foo_context' + messages.forEach( message => onFoundMessage( message ) ); } ); createPotFiles( { @@ -414,67 +740,103 @@ msgstr "foo" packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger, + logger: loggerMocks, ignoreUnusedCorePackageContexts: true } ); - sinon.assert.calledOnce( stubs.logger.error ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' + ); + + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 3, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); - sinon.assert.calledWithExactly( - stubs.logger.error, + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); + + expect( loggerMocks.error ).toHaveBeenCalledTimes( 1 ); + expect( loggerMocks.error ).toHaveBeenCalledWith( 'Unused context: \'bar_id\' in ckeditor5-foo/lang/contexts.json' ); // Mark the process as failed in case of the error. - expect( process.exitCode ).to.equal( 1 ); + expect( process.exitCode ).toEqual( 1 ); } ); it( 'should not add the license header in the created a POT file entry when skipLicenseHeader=true', () => { - createFakeContextFile( 'packages/ckeditor5-foo/lang/contexts.json', { foo_id: 'foo_context' } ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( true ); + vi.mocked( fs.existsSync ).mockReturnValueOnce( false ); - createFakeSourceFileWithMessages( 'packages/ckeditor5-foo/src/foo.js', [ - { string: 'foo', id: 'foo_id' } - ] ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( JSON.stringify( { 'foo_id': 'foo_context' } ) ); + vi.mocked( fs.readFileSync ).mockReturnValueOnce( 'packages/ckeditor5-foo/src/foo.js_content' ); + + vi.mocked( findMessages ).mockImplementationOnce( ( fileContent, filePath, onFoundMessage ) => { + const messages = [ + { string: 'foo', id: 'foo_id' } + ]; + + messages.forEach( message => onFoundMessage( message ) ); + } ); createPotFiles( { sourceFiles: [ 'packages/ckeditor5-foo/src/foo.js' ], packagePaths: [ 'packages/ckeditor5-foo' ], corePackagePath: 'packages/ckeditor5-core', translationsDirectory: '/cwd/build/.transifex', - logger: stubs.logger, + logger: loggerMocks, skipLicenseHeader: true } ); - sinon.assert.calledOnce( stubs.fs.outputFileSync ); - - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync, - '/cwd/build/.transifex/ckeditor5-foo/en.pot', - `msgctxt "foo_context" -msgid "foo_id" -msgstr "foo" -` + expect( vi.mocked( fs.existsSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json' + ); + expect( vi.mocked( fs.existsSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-core/lang/contexts.json' ); - } ); - - function createFakeSourceFileWithMessages( file, messages, errors = [] ) { - const content = file + '_content'; - - stubs.fs.readFileSync - .withArgs( file ).returns( content ); - stubs.translations.findMessages - .withArgs( content ).callsFake( ( fileContent, filePath, onFoundMessage, onErrorFound ) => { - messages.forEach( message => onFoundMessage( message ) ); - errors.forEach( error => onErrorFound( error ) ); - } ); - } + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 1, 'packages/ckeditor5-foo/lang/contexts.json', 'utf-8' + ); + expect( vi.mocked( fs.readFileSync ) ).toHaveBeenNthCalledWith( + 2, 'packages/ckeditor5-foo/src/foo.js', 'utf-8' + ); - function createFakeContextFile( pathToContext, content ) { - stubs.fs.readFileSync - .withArgs( pathToContext ).returns( JSON.stringify( content ) ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( findMessages ) ).toHaveBeenCalledWith( + 'packages/ckeditor5-foo/src/foo.js_content', + 'packages/ckeditor5-foo/src/foo.js', + expect.any( Function ), + expect.any( Function ) + ); - stubs.fs.existsSync - .withArgs( pathToContext ).returns( true ); - } + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledWith( + '/cwd/build/.transifex/ckeditor5-foo/en.pot', + [ + 'msgctxt "foo_context"', + 'msgid "foo_id"', + 'msgstr "foo"', + '' + ].join( '\n' ) + ); + } ); } ); diff --git a/packages/ckeditor5-dev-transifex/tests/download.js b/packages/ckeditor5-dev-transifex/tests/download.js index 5874eff1a..547950723 100644 --- a/packages/ckeditor5-dev-transifex/tests/download.js +++ b/packages/ckeditor5-dev-transifex/tests/download.js @@ -3,143 +3,103 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); -const { expect } = require( 'chai' ); +import path from 'path'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import download from '../lib/download.js'; + +import { cleanPoFileContent, createDictionaryFromPoFileContent } from '@ckeditor/ckeditor5-dev-translations'; +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import { verifyProperties, createLogger } from '../lib/utils.js'; +import fs from 'fs-extra'; +import transifexService from '../lib/transifexservice.js'; + +vi.mock( '../lib/transifexservice.js' ); +vi.mock( '../lib/utils.js' ); +vi.mock( '@ckeditor/ckeditor5-dev-translations' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( 'fs-extra' ); + +vi.mock( 'chalk', () => ( { + default: { + underline: vi.fn( string => string ), + gray: vi.fn( string => string ) + } +} ) ); + +vi.mock( '../lib/data/index.js', () => { + return { + languageCodeMap: { + ne_NP: 'ne' + } + }; +} ); describe( 'dev-transifex/download()', () => { - let stubs, mocks, download; + let mocks; + let loggerProgressMock, loggerInfoMock, loggerWarningMock, loggerErrorMock, loggerLogMock; + let spinnerStartMock, spinnerFinishMock; beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false + loggerProgressMock = vi.fn(); + loggerInfoMock = vi.fn(); + loggerWarningMock = vi.fn(); + loggerErrorMock = vi.fn(); + loggerErrorMock = vi.fn(); + + vi.mocked( createLogger ).mockImplementation( () => { + return { + progress: loggerProgressMock, + info: loggerInfoMock, + warning: loggerWarningMock, + error: loggerErrorMock, + _log: loggerLogMock + }; } ); - stubs = { - logger: { - progress: sinon.stub(), - info: sinon.stub(), - warning: sinon.stub(), - error: sinon.stub(), - _log: sinon.stub() - }, - - fs: { - outputFileSync: sinon.stub(), - - removeSync: sinon.stub(), - - existsSync: sinon.stub() - .withArgs( path.normalize( '/workspace/.transifex-failed-downloads.json' ) ) - .callsFake( () => Boolean( mocks.oldFailedDownloads ) ), - - readJsonSync: sinon.stub() - .withArgs( path.normalize( '/workspace/.transifex-failed-downloads.json' ) ) - .callsFake( () => mocks.oldFailedDownloads ), - - writeJsonSync: sinon.stub() - }, - - translationUtils: { - createDictionaryFromPoFileContent: sinon.stub().callsFake( fileContent => mocks.fileContents[ fileContent ] ), - cleanPoFileContent: sinon.stub().callsFake( fileContent => fileContent ) - }, - - chalk: { - underline: sinon.stub().callsFake( msg => msg ), - gray: sinon.stub().callsFake( msg => msg ) - }, + spinnerStartMock = vi.fn(); + spinnerFinishMock = vi.fn(); - tools: { - createSpinner: sinon.stub(), - spinnerStart: sinon.stub(), - spinnerFinish: sinon.stub() - }, + vi.mocked( tools.createSpinner ).mockReturnValue( { + start: spinnerStartMock, + finish: spinnerFinishMock + } ); - transifexService: { - init: sinon.stub(), - - getResourceName: sinon.stub().callsFake( resource => resource.attributes.slug ), - - getLanguageCode: sinon.stub().callsFake( language => language.attributes.code ), - - getProjectData: sinon.stub().callsFake( ( organizationName, projectName, localizablePackageNames ) => { - const projectData = { - resources: mocks.resources.filter( resource => localizablePackageNames.includes( resource.attributes.slug ) ), - languages: mocks.languages - }; - - return Promise.resolve( projectData ); - } ), - - getTranslations: sinon.stub().callsFake( ( resource, languages ) => { - const translationData = { - translations: new Map( - languages.map( language => [ - language.attributes.code, - mocks.translations[ resource.attributes.slug ][ language.attributes.code ] - ] ) - ), - failedDownloads: mocks.newFailedDownloads ? - mocks.newFailedDownloads.filter( item => { - const isResourceNameMatched = item.resourceName === resource.attributes.slug; - const isLanguageCodeMatched = languages.find( language => item.languageCode === language.attributes.code ); - - return isResourceNameMatched && isLanguageCodeMatched; - } ) : - [] - }; - - return Promise.resolve( translationData ); - } ) - }, + vi.mocked( fs.existsSync ).mockImplementation( () => Boolean( mocks.oldFailedDownloads ) ); + vi.mocked( fs.readJsonSync ).mockImplementation( () => mocks.oldFailedDownloads ); - utils: { - verifyProperties: sinon.stub(), - createLogger: sinon.stub() - } - }; + vi.mocked( createDictionaryFromPoFileContent ).mockImplementation( fileContent => mocks.fileContents[ fileContent ] ); + vi.mocked( cleanPoFileContent ).mockImplementation( fileContent => fileContent ); - stubs.tools.createSpinner.returns( { - start: stubs.tools.spinnerStart, - finish: stubs.tools.spinnerFinish - } ); - - stubs.utils.createLogger.returns( { - progress: stubs.logger.progress, - info: stubs.logger.info, - warning: stubs.logger.warning, - error: stubs.logger.error, - _log: sinon.stub() - } ); + vi.mocked( transifexService.getResourceName ).mockImplementation( resource => resource.attributes.slug ); + vi.mocked( transifexService.getLanguageCode ).mockImplementation( language => language.attributes.code ); + vi.mocked( transifexService.getProjectData ).mockImplementation( ( organizationName, projectName, localizablePackageNames ) => { + const projectData = { + resources: mocks.resources.filter( resource => localizablePackageNames.includes( resource.attributes.slug ) ), + languages: mocks.languages + }; - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { - tools: stubs.tools + return Promise.resolve( projectData ); } ); + vi.mocked( transifexService.getTranslations ).mockImplementation( ( resource, languages ) => { + const translationData = { + translations: new Map( + languages.map( language => [ + language.attributes.code, + mocks.translations[ resource.attributes.slug ][ language.attributes.code ] + ] ) + ), + failedDownloads: mocks.newFailedDownloads ? + mocks.newFailedDownloads.filter( item => { + const isResourceNameMatched = item.resourceName === resource.attributes.slug; + const isLanguageCodeMatched = languages.find( language => item.languageCode === language.attributes.code ); + + return isResourceNameMatched && isLanguageCodeMatched; + } ) : + [] + }; - mockery.registerMock( '@ckeditor/ckeditor5-dev-translations', { - cleanPoFileContent: stubs.translationUtils.cleanPoFileContent, - createDictionaryFromPoFileContent: stubs.translationUtils.createDictionaryFromPoFileContent + return Promise.resolve( translationData ); } ); - - mockery.registerMock( 'fs-extra', stubs.fs ); - mockery.registerMock( 'chalk', stubs.chalk ); - mockery.registerMock( './transifexservice', stubs.transifexService ); - mockery.registerMock( './utils', stubs.utils ); - mockery.registerMock( './languagecodemap.json', { ne_NP: 'ne' } ); - - download = require( '../lib/download' ); - } ); - - afterEach( () => { - sinon.restore(); - mockery.deregisterAll(); - mockery.disable(); } ); it( 'should fail if properties verification failed', () => { @@ -151,25 +111,22 @@ describe( 'dev-transifex/download()', () => { token: 'secretToken' }; - stubs.utils.verifyProperties.throws( error ); + vi.mocked( verifyProperties ).mockImplementation( () => { + throw new Error( error ); + } ); return download( config ) .then( () => { throw new Error( 'Expected to be rejected.' ); }, - err => { - expect( err ).to.equal( error ); - - expect( stubs.utils.verifyProperties.callCount ).to.equal( 1 ); - expect( stubs.utils.verifyProperties.firstCall.args[ 0 ] ).to.deep.equal( config ); - expect( stubs.utils.verifyProperties.firstCall.args[ 1 ] ).to.deep.equal( [ - 'organizationName', - 'projectName', - 'token', - 'packages', - 'cwd' - ] ); + caughtError => { + expect( caughtError.message.endsWith( error.message ) ).toEqual( true ); + + expect( vi.mocked( verifyProperties ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( verifyProperties ) ).toHaveBeenCalledWith( + config, [ 'organizationName', 'projectName', 'token', 'packages', 'cwd' ] + ); } ); } ); @@ -202,21 +159,24 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.calledTwice( stubs.fs.removeSync ); - sinon.assert.calledOnce( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.removeSync ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 1 ); - sinon.assert.calledWithExactly( - stubs.fs.removeSync.firstCall, + expect( vi.mocked( fs.removeSync ) ).toHaveBeenNthCalledWith( + 1, path.normalize( '/workspace/foo/ckeditor5-core/lang/translations' ) ); - - sinon.assert.calledWithExactly( - stubs.fs.removeSync.secondCall, + expect( vi.mocked( fs.removeSync ) ).toHaveBeenNthCalledWith( + 2, path.normalize( '/workspace/.transifex-failed-downloads.json' ) ); - expect( stubs.fs.removeSync.firstCall.calledBefore( stubs.fs.outputFileSync.firstCall ) ).to.be.true; - expect( stubs.fs.removeSync.secondCall.calledAfter( stubs.fs.outputFileSync.firstCall ) ).to.be.true; + const removeSyncMockFirstCallOrder = vi.mocked( fs.removeSync ).mock.invocationCallOrder[ 0 ]; + const removeSyncMockSecondCallOrder = vi.mocked( fs.removeSync ).mock.invocationCallOrder[ 1 ]; + const outputFileSyncMockFirstCallOrder = vi.mocked( fs.outputFileSync ).mock.invocationCallOrder[ 0 ]; + + expect( removeSyncMockFirstCallOrder < outputFileSyncMockFirstCallOrder ).toEqual( true ); + expect( outputFileSyncMockFirstCallOrder < removeSyncMockSecondCallOrder ).toEqual( true ); } ); it( 'should download translations for non-empty resources', async () => { @@ -258,34 +218,44 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.callCount( stubs.fs.outputFileSync, 3 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 3 ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync.firstCall, + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenNthCalledWith( + 1, path.normalize( '/workspace/foo/ckeditor5-core/lang/translations/pl.po' ), 'ckeditor5-core-pl-content' ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync.secondCall, + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenNthCalledWith( + 2, path.normalize( '/workspace/foo/ckeditor5-core/lang/translations/de.po' ), 'ckeditor5-core-de-content' ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync.thirdCall, + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenNthCalledWith( + 3, path.normalize( '/workspace/bar/ckeditor5-ui/lang/translations/pl.po' ), 'ckeditor5-ui-pl-content' ); - sinon.assert.callCount( stubs.logger.progress, 3 ); - sinon.assert.calledWithExactly( stubs.logger.progress.firstCall, 'Fetching project information...' ); - sinon.assert.calledWithExactly( stubs.logger.progress.secondCall, 'Downloading all translations...' ); - sinon.assert.calledWithExactly( stubs.logger.progress.thirdCall, 'Saved all translations.' ); + expect( vi.mocked( loggerProgressMock ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( loggerProgressMock ) ).toHaveBeenNthCalledWith( + 1, 'Fetching project information...' + ); + expect( vi.mocked( loggerProgressMock ) ).toHaveBeenNthCalledWith( + 2, 'Downloading all translations...' + ); + expect( vi.mocked( loggerProgressMock ) ).toHaveBeenNthCalledWith( + 3, 'Saved all translations.' + ); - sinon.assert.callCount( stubs.logger.info, 2 ); - sinon.assert.calledWithExactly( stubs.logger.info.firstCall, ' Saved 2 "*.po" file(s).' ); - sinon.assert.calledWithExactly( stubs.logger.info.secondCall, ' Saved 1 "*.po" file(s).' ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenNthCalledWith( + 1, ' Saved 2 "*.po" file(s).' + ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenNthCalledWith( + 2, ' Saved 1 "*.po" file(s).' + ); } ); it( 'should download translations for non-empty resources only for specified packages', async () => { @@ -326,10 +296,9 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.callCount( stubs.fs.outputFileSync, 1 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 1 ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync, + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledWith( path.normalize( '/workspace/bar/ckeditor5-ui/lang/translations/pl.po' ), 'ckeditor5-ui-pl-content' ); @@ -374,18 +343,18 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.callCount( stubs.tools.createSpinner, 2 ); - sinon.assert.callCount( stubs.tools.spinnerStart, 2 ); - sinon.assert.callCount( stubs.tools.spinnerFinish, 2 ); + expect( vi.mocked( tools.createSpinner ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( spinnerStartMock ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( spinnerFinishMock ) ).toHaveBeenCalledTimes( 2 ); - sinon.assert.calledWithExactly( - stubs.tools.createSpinner.firstCall, + expect( vi.mocked( tools.createSpinner ) ).toHaveBeenNthCalledWith( + 1, 'Processing "ckeditor5-core"...', { indentLevel: 1, emoji: '👉' } ); - sinon.assert.calledWithExactly( - stubs.tools.createSpinner.secondCall, + expect( vi.mocked( tools.createSpinner ) ).toHaveBeenNthCalledWith( + 2, 'Processing "ckeditor5-ui"...', { indentLevel: 1, emoji: '👉' } ); @@ -409,7 +378,7 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.notCalled( stubs.fs.outputFileSync ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 0 ); } ); it( 'should save failed downloads', async () => { @@ -454,37 +423,36 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.calledOnce( stubs.fs.writeJsonSync ); + expect( vi.mocked( fs.writeJSONSync ) ).toHaveBeenCalledTimes( 1 ); - sinon.assert.calledWithExactly( - stubs.fs.writeJsonSync, + expect( vi.mocked( fs.writeJSONSync ) ).toHaveBeenCalledWith( path.normalize( '/workspace/.transifex-failed-downloads.json' ), [ { resourceName: 'ckeditor5-ui', languages: [ { code: 'de', errorMessage: 'An example error.' } ] } ], { spaces: 2 } ); - sinon.assert.callCount( stubs.logger.info, 2 ); - sinon.assert.calledWithExactly( stubs.logger.info.firstCall, ' Saved 2 "*.po" file(s).' ); - sinon.assert.calledWithExactly( stubs.logger.info.secondCall, ' Saved 2 "*.po" file(s). 1 requests failed.' ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenNthCalledWith( 1, ' Saved 2 "*.po" file(s).' ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenNthCalledWith( 2, ' Saved 2 "*.po" file(s). 1 requests failed.' ); - sinon.assert.callCount( stubs.logger.warning, 3 ); - sinon.assert.calledWithExactly( - stubs.logger.warning.firstCall, + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 1, 'Not all translations were downloaded due to errors in Transifex API.' ); - sinon.assert.calledWithExactly( - stubs.logger.warning.secondCall, + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 2, `Review the "${ path.normalize( '/workspace/.transifex-failed-downloads.json' ) }" file for more details.` ); - sinon.assert.calledWithExactly( - stubs.logger.warning.thirdCall, + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 3, 'Re-running the script will process only packages specified in the file.' ); - sinon.assert.callCount( stubs.tools.spinnerFinish, 2 ); + expect( vi.mocked( spinnerFinishMock ) ).toHaveBeenCalledTimes( 2 ); // First call: OK. Second call: error. - sinon.assert.calledWithExactly( stubs.tools.spinnerFinish ); - sinon.assert.calledWithExactly( stubs.tools.spinnerFinish, { emoji: '❌' } ); + expect( vi.mocked( spinnerFinishMock ) ).toHaveBeenNthCalledWith( 1 ); + expect( vi.mocked( spinnerFinishMock ) ).toHaveBeenNthCalledWith( 2, { emoji: '❌' } ); } ); it( 'should use the language code from the "languagecodemap.json" if it exists, or the default language code otherwise', async () => { @@ -521,22 +489,22 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.callCount( stubs.fs.outputFileSync, 3 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 3 ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync.firstCall, + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenNthCalledWith( + 1, path.normalize( '/workspace/foo/ckeditor5-core/lang/translations/pl.po' ), 'ckeditor5-core-pl-content' ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync.secondCall, + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenNthCalledWith( + 2, path.normalize( '/workspace/foo/ckeditor5-core/lang/translations/en_AU.po' ), 'ckeditor5-core-en_AU-content' ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync.thirdCall, + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenNthCalledWith( + 3, path.normalize( '/workspace/foo/ckeditor5-core/lang/translations/ne.po' ), 'ckeditor5-core-ne_NP-content' ); @@ -545,7 +513,7 @@ describe( 'dev-transifex/download()', () => { it( 'should fail with an error when the transifex service responses with an error', async () => { const error = new Error( 'An example error.' ); - stubs.transifexService.getProjectData.rejects( error ); + vi.mocked( transifexService.getProjectData ).mockRejectedValue( error ); try { await download( { @@ -562,7 +530,7 @@ describe( 'dev-transifex/download()', () => { expect( err ).to.equal( error ); } - expect( stubs.transifexService.getProjectData.called ).to.equal( true ); + expect( vi.mocked( transifexService.getProjectData ) ).toHaveBeenCalled(); } ); it( 'should pass the "simplifyLicenseHeader" flag to the "cleanPoFileContent()" function when set to `true`', async () => { @@ -595,10 +563,9 @@ describe( 'dev-transifex/download()', () => { simplifyLicenseHeader: true } ); - sinon.assert.calledOnce( stubs.translationUtils.cleanPoFileContent ); + expect( vi.mocked( cleanPoFileContent ) ).toHaveBeenCalledTimes( 1 ); - sinon.assert.calledWithExactly( - stubs.translationUtils.cleanPoFileContent, + expect( vi.mocked( cleanPoFileContent ) ).toHaveBeenCalledWith( 'ckeditor5-core-pl-content', { simplifyLicenseHeader: true @@ -638,15 +605,17 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.calledOnce( stubs.fs.outputFileSync ); - sinon.assert.calledOnce( stubs.fs.removeSync ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.removeSync ) ).toHaveBeenCalledTimes( 1 ); - sinon.assert.calledWithExactly( - stubs.fs.removeSync, + expect( vi.mocked( fs.removeSync ) ).toHaveBeenCalledWith( path.normalize( '/workspace/.transifex-failed-downloads.json' ) ); - expect( stubs.fs.removeSync.calledAfter( stubs.fs.outputFileSync ) ).to.be.true; + const outputFileSyncMockFirstCallOrder = vi.mocked( fs.outputFileSync ).mock.invocationCallOrder[ 0 ]; + const removeSyncMockFirstCallOrder = vi.mocked( fs.removeSync ).mock.invocationCallOrder[ 0 ]; + + expect( outputFileSyncMockFirstCallOrder < removeSyncMockFirstCallOrder ).toEqual( true ); } ); it( 'should download translations for existing resources but only for previously failed ones', async () => { @@ -692,28 +661,27 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.callCount( stubs.fs.outputFileSync, 1 ); + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledTimes( 1 ); - sinon.assert.calledWithExactly( - stubs.fs.outputFileSync, + expect( vi.mocked( fs.outputFileSync ) ).toHaveBeenCalledWith( path.normalize( '/workspace/foo/ckeditor5-core/lang/translations/pl.po' ), 'ckeditor5-core-pl-content' ); - sinon.assert.callCount( stubs.logger.warning, 2 ); - sinon.assert.calledWithExactly( - stubs.logger.warning.firstCall, + expect( loggerWarningMock ).toHaveBeenCalledTimes( 2 ); + expect( loggerWarningMock ).toHaveBeenNthCalledWith( + 1, 'Found the file containing a list of packages that failed during the last script execution.' ); - sinon.assert.calledWithExactly( - stubs.logger.warning.secondCall, + expect( loggerWarningMock ).toHaveBeenNthCalledWith( + 2, 'The script will process only packages listed in the file instead of all passed as "config.packages".' ); - sinon.assert.callCount( stubs.logger.progress, 3 ); - sinon.assert.calledWithExactly( stubs.logger.progress.firstCall, 'Fetching project information...' ); - sinon.assert.calledWithExactly( stubs.logger.progress.secondCall, 'Downloading only translations that failed previously...' ); - sinon.assert.calledWithExactly( stubs.logger.progress.thirdCall, 'Saved all translations.' ); + expect( loggerProgressMock ).toHaveBeenCalledTimes( 3 ); + expect( loggerProgressMock ).toHaveBeenNthCalledWith( 1, 'Fetching project information...' ); + expect( loggerProgressMock ).toHaveBeenNthCalledWith( 2, 'Downloading only translations that failed previously...' ); + expect( loggerProgressMock ).toHaveBeenNthCalledWith( 3, 'Saved all translations.' ); } ); it( 'should update ".transifex-failed-downloads.json" file if there are still some failed downloads', async () => { @@ -762,10 +730,9 @@ describe( 'dev-transifex/download()', () => { ] ) } ); - sinon.assert.calledOnce( stubs.fs.writeJsonSync ); + expect( vi.mocked( fs.writeJSONSync ) ).toHaveBeenCalledTimes( 1 ); - sinon.assert.calledWithExactly( - stubs.fs.writeJsonSync, + expect( vi.mocked( fs.writeJSONSync ) ).toHaveBeenCalledWith( path.normalize( '/workspace/.transifex-failed-downloads.json' ), [ { resourceName: 'ckeditor5-core', languages: [ { code: 'de', errorMessage: 'An example error.' } ] } ], { spaces: 2 } diff --git a/packages/ckeditor5-dev-transifex/tests/transifexservice.js b/packages/ckeditor5-dev-transifex/tests/transifexservice.js index 7caf62ed0..552ddcfad 100644 --- a/packages/ckeditor5-dev-transifex/tests/transifexservice.js +++ b/packages/ckeditor5-dev-transifex/tests/transifexservice.js @@ -3,137 +3,131 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import transifexService from '../lib/transifexservice.js'; + +const { + transifexApiMock +} = vi.hoisted( () => { + return { + transifexApiMock: {} + }; +} ); -const chai = require( 'chai' ); -const sinon = require( 'sinon' ); -const expect = chai.expect; -const mockery = require( 'mockery' ); +vi.mock( '@transifex/api', () => { + return { + transifexApi: transifexApiMock + }; +} ); describe( 'dev-transifex/transifex-service', () => { - let stubs, mocks, transifexService; + let testData; + + let fetchMock; + + let createResourceMock; + let createResourceStringsAsyncUploadMock; + let dataResourceTranslationsMock; + let fetchOrganizationMock; + let fetchProjectMock; + let fetchResourceTranslationsMock; + let filterResourceTranslationsMock; + let getNextResourceTranslationsMock; + let getOrganizationsMock; + let getProjectsMock; + let getResourceStringsAsyncUploadMock; + let includeResourceTranslationsMock; beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - getOrganizations: sinon.stub().callsFake( () => Promise.resolve( { - fetch: stubs.fetchOrganization - } ) ), - - fetchOrganization: sinon.stub().callsFake( () => Promise.resolve( { - get: stubs.getProjects - } ) ), - - getProjects: sinon.stub().callsFake( () => Promise.resolve( { - fetch: stubs.fetchProject - } ) ), - - fetchProject: sinon.stub().callsFake( resourceType => Promise.resolve( { - async* all() { - for ( const item of mocks[ resourceType ] ) { - yield item; - } + fetchMock = vi.fn(); + + vi.stubGlobal( 'fetch', fetchMock ); + + createResourceMock = vi.fn(); + createResourceStringsAsyncUploadMock = vi.fn(); + fetchResourceTranslationsMock = vi.fn(); + filterResourceTranslationsMock = vi.fn(); + getNextResourceTranslationsMock = vi.fn(); + getResourceStringsAsyncUploadMock = vi.fn(); + includeResourceTranslationsMock = vi.fn(); + + getOrganizationsMock = vi.fn().mockImplementation( () => Promise.resolve( { + fetch: fetchOrganizationMock + } ) ); + + fetchOrganizationMock = vi.fn().mockImplementation( () => Promise.resolve( { + get: getProjectsMock + } ) ); + + getProjectsMock = vi.fn().mockImplementation( () => Promise.resolve( { + fetch: fetchProjectMock + } ) ); + + fetchProjectMock = vi.fn().mockImplementation( resourceType => Promise.resolve( { + async* all() { + for ( const item of testData[ resourceType ] ) { + yield item; } - } ) ), + } + } ) ); - createResource: sinon.stub(), - createResourceStringsAsyncUpload: sinon.stub(), - getResourceStringsAsyncUpload: sinon.stub(), + transifexApiMock.setup = vi.fn().mockImplementation( ( { auth } ) => { + transifexApiMock.auth = vi.fn().mockReturnValue( { Authorization: `Bearer ${ auth }` } ); + } ); - filterResourceTranslations: sinon.stub(), - includeResourceTranslations: sinon.stub(), - fetchResourceTranslations: sinon.stub(), - getNextResourceTranslations: sinon.stub(), - dataResourceTranslations: [], + transifexApiMock.Organization = { + get: ( ...args ) => getOrganizationsMock( ...args ) + }; - transifexApi: { - setup: sinon.stub().callsFake( ( { auth } ) => { - stubs.transifexApi.auth = sinon.stub().returns( { Authorization: `Bearer ${ auth }` } ); - } ), + transifexApiMock.Resource = { + create: ( ...args ) => createResourceMock( ...args ) + }; - Organization: { - get: ( ...args ) => stubs.getOrganizations( ...args ) - }, + transifexApiMock.ResourceStringsAsyncUpload = { + create: ( ...args ) => createResourceStringsAsyncUploadMock( ...args ), + get: ( ...args ) => getResourceStringsAsyncUploadMock( ...args ) + }; - Resource: { - create: ( ...args ) => stubs.createResource( ...args ) - }, + transifexApiMock.ResourceStringsAsyncDownload = resourceAsyncDownloadMockFactory(); + transifexApiMock.ResourceTranslationsAsyncDownload = resourceAsyncDownloadMockFactory(); - ResourceStringsAsyncUpload: { - create: ( ...args ) => stubs.createResourceStringsAsyncUpload( ...args ), - get: ( ...args ) => stubs.getResourceStringsAsyncUpload( ...args ) - }, + transifexApiMock.ResourceTranslation = { + filter: ( ...args ) => { + filterResourceTranslationsMock( ...args ); - ...[ 'ResourceStringsAsyncDownload', 'ResourceTranslationsAsyncDownload' ].reduce( ( result, methodName ) => { - result[ methodName ] = { - create: sinon.stub().callsFake( ( { attributes, relationships, type } ) => { - const resourceName = relationships.resource.attributes.slug; - const languageCode = relationships.language ? relationships.language.attributes.code : 'en'; - - return Promise.resolve( { - attributes, - type, - links: { - self: `https://example.com/${ resourceName }/${ languageCode }` - }, - related: relationships - } ); - } ) - }; - - return result; - }, {} ), - - ResourceTranslation: { - filter: ( ...args ) => { - stubs.filterResourceTranslations( ...args ); + return { + include: ( ...args ) => { + includeResourceTranslationsMock( ...args ); return { - include: ( ...args ) => { - stubs.includeResourceTranslations( ...args ); - - return { - fetch: ( ...args ) => stubs.fetchResourceTranslations( ...args ), - get data() { - return stubs.dataResourceTranslations; - }, - get next() { - return !!stubs.getNextResourceTranslations; - }, - getNext: () => stubs.getNextResourceTranslations() - }; - } + fetch: ( ...args ) => fetchResourceTranslationsMock( ...args ), + get data() { + return dataResourceTranslationsMock; + }, + get next() { + return !!getNextResourceTranslationsMock; + }, + getNext: () => getNextResourceTranslationsMock() }; } - } - }, - - fetch: sinon.stub() + }; + } }; - - mockery.registerMock( '@transifex/api', { transifexApi: stubs.transifexApi } ); - mockery.registerMock( 'node-fetch', stubs.fetch ); - - transifexService = require( '../lib/transifexservice' ); } ); afterEach( () => { - sinon.restore(); - mockery.deregisterAll(); - mockery.disable(); + vi.useRealTimers(); + + // Restoring mock of Transifex API. + Object.keys( transifexApiMock ).forEach( mockedKey => delete transifexApiMock[ mockedKey ] ); } ); describe( 'init()', () => { it( 'should pass the token to the Transifex API', () => { transifexService.init( 'secretToken' ); - expect( stubs.transifexApi.auth ).to.be.a( 'function' ); - expect( stubs.transifexApi.auth() ).to.deep.equal( { Authorization: 'Bearer secretToken' } ); + expect( transifexApiMock.auth ).toBeInstanceOf( Function ); + expect( transifexApiMock.auth() ).toEqual( { Authorization: 'Bearer secretToken' } ); } ); it( 'should pass the token to the Transifex API only once', () => { @@ -141,16 +135,16 @@ describe( 'dev-transifex/transifex-service', () => { transifexService.init( 'anotherSecretToken' ); transifexService.init( 'evenBetterSecretToken' ); - sinon.assert.calledOnce( stubs.transifexApi.setup ); + expect( transifexApiMock.setup ).toHaveBeenCalledTimes( 1 ); - expect( stubs.transifexApi.auth ).to.be.a( 'function' ); - expect( stubs.transifexApi.auth() ).to.deep.equal( { Authorization: 'Bearer secretToken' } ); + expect( transifexApiMock.auth ).toBeInstanceOf( Function ); + expect( transifexApiMock.auth() ).toEqual( { Authorization: 'Bearer secretToken' } ); } ); } ); describe( 'getProjectData()', () => { it( 'should return resources and languages, with English language as the source one', async () => { - mocks = { + testData = { resources: [ { attributes: { slug: 'ckeditor5-core' } }, { attributes: { slug: 'ckeditor5-ui' } } @@ -165,25 +159,25 @@ describe( 'dev-transifex/transifex-service', () => { 'ckeditor-organization', 'ckeditor5-project', [ 'ckeditor5-core', 'ckeditor5-ui' ] ); - sinon.assert.calledOnce( stubs.getOrganizations ); - sinon.assert.calledWithExactly( stubs.getOrganizations, { slug: 'ckeditor-organization' } ); + expect( getOrganizationsMock ).toHaveBeenCalledTimes( 1 ); + expect( getOrganizationsMock ).toHaveBeenCalledWith( { slug: 'ckeditor-organization' } ); - sinon.assert.calledOnce( stubs.fetchOrganization ); - sinon.assert.calledWithExactly( stubs.fetchOrganization, 'projects' ); + expect( fetchOrganizationMock ).toHaveBeenCalledTimes( 1 ); + expect( fetchOrganizationMock ).toHaveBeenCalledWith( 'projects' ); - sinon.assert.calledOnce( stubs.getProjects ); - sinon.assert.calledWithExactly( stubs.getProjects, { slug: 'ckeditor5-project' } ); + expect( getProjectsMock ).toHaveBeenCalledTimes( 1 ); + expect( getProjectsMock ).toHaveBeenCalledWith( { slug: 'ckeditor5-project' } ); - sinon.assert.calledTwice( stubs.fetchProject ); - sinon.assert.calledWithExactly( stubs.fetchProject.firstCall, 'resources' ); - sinon.assert.calledWithExactly( stubs.fetchProject.secondCall, 'languages' ); + expect( fetchProjectMock ).toHaveBeenCalledTimes( 2 ); + expect( fetchProjectMock ).toHaveBeenNthCalledWith( 1, 'resources' ); + expect( fetchProjectMock ).toHaveBeenNthCalledWith( 2, 'languages' ); - expect( resources ).to.deep.equal( [ + expect( resources ).toEqual( [ { attributes: { slug: 'ckeditor5-core' } }, { attributes: { slug: 'ckeditor5-ui' } } ] ); - expect( languages ).to.deep.equal( [ + expect( languages ).toEqual( [ { attributes: { code: 'en' } }, { attributes: { code: 'pl' } }, { attributes: { code: 'de' } } @@ -191,7 +185,7 @@ describe( 'dev-transifex/transifex-service', () => { } ); it( 'should return only the available resources that were requested', async () => { - mocks = { + testData = { resources: [ { attributes: { slug: 'ckeditor5-core' } }, { attributes: { slug: 'ckeditor5-ui' } } @@ -206,11 +200,11 @@ describe( 'dev-transifex/transifex-service', () => { 'ckeditor-organization', 'ckeditor5-project', [ 'ckeditor5-core', 'ckeditor5-non-existing' ] ); - expect( resources ).to.deep.equal( [ + expect( resources ).toEqual( [ { attributes: { slug: 'ckeditor5-core' } } ] ); - expect( languages ).to.deep.equal( [ + expect( languages ).toEqual( [ { attributes: { code: 'en' } }, { attributes: { code: 'pl' } }, { attributes: { code: 'de' } } @@ -220,7 +214,7 @@ describe( 'dev-transifex/transifex-service', () => { describe( 'getTranslations()', () => { beforeEach( () => { - mocks = { + testData = { resources: [ { attributes: { slug: 'ckeditor5-core' } }, { attributes: { slug: 'ckeditor5-ui' } } @@ -244,14 +238,14 @@ describe( 'dev-transifex/transifex-service', () => { } ); it( 'should return requested translations if no retries are needed', async () => { - stubs.fetch.callsFake( url => Promise.resolve( { + vi.mocked( fetchMock ).mockImplementation( url => Promise.resolve( { ok: true, redirected: true, - text: () => Promise.resolve( mocks.translations[ url ] ) + text: () => Promise.resolve( testData.translations[ url ] ) } ) ); - const resource = mocks.resources[ 0 ]; - const languages = [ ...mocks.languages ]; + const resource = testData.resources[ 0 ]; + const languages = [ ...testData.languages ]; const { translations, failedDownloads } = await transifexService.getTranslations( resource, languages ); const attributes = { @@ -261,9 +255,9 @@ describe( 'dev-transifex/transifex-service', () => { pseudo: false }; - sinon.assert.calledOnce( stubs.transifexApi.ResourceStringsAsyncDownload.create ); + expect( transifexApiMock.ResourceStringsAsyncDownload.create ).toHaveBeenCalledTimes( 1 ); - sinon.assert.calledWithExactly( stubs.transifexApi.ResourceStringsAsyncDownload.create, { + expect( transifexApiMock.ResourceStringsAsyncDownload.create ).toHaveBeenCalledWith( { attributes, relationships: { resource @@ -271,9 +265,9 @@ describe( 'dev-transifex/transifex-service', () => { type: 'resource_strings_async_downloads' } ); - sinon.assert.calledTwice( stubs.transifexApi.ResourceTranslationsAsyncDownload.create ); + expect( transifexApiMock.ResourceTranslationsAsyncDownload.create ).toHaveBeenCalledTimes( 2 ); - sinon.assert.calledWithExactly( stubs.transifexApi.ResourceTranslationsAsyncDownload.create.firstCall, { + expect( transifexApiMock.ResourceTranslationsAsyncDownload.create ).toHaveBeenNthCalledWith( 1, { attributes, relationships: { resource, @@ -282,7 +276,7 @@ describe( 'dev-transifex/transifex-service', () => { type: 'resource_translations_async_downloads' } ); - sinon.assert.calledWithExactly( stubs.transifexApi.ResourceTranslationsAsyncDownload.create.secondCall, { + expect( transifexApiMock.ResourceTranslationsAsyncDownload.create ).toHaveBeenNthCalledWith( 2, { attributes, relationships: { resource, @@ -291,95 +285,94 @@ describe( 'dev-transifex/transifex-service', () => { type: 'resource_translations_async_downloads' } ); - sinon.assert.calledThrice( stubs.fetch ); + expect( fetchMock ).toHaveBeenCalledTimes( 3 ); - sinon.assert.calledWithExactly( stubs.fetch.firstCall, 'https://example.com/ckeditor5-core/en', { + expect( fetchMock ).toHaveBeenNthCalledWith( 1, 'https://example.com/ckeditor5-core/en', { + method: 'GET', headers: { Authorization: 'Bearer secretToken' } } ); - sinon.assert.calledWithExactly( stubs.fetch.secondCall, 'https://example.com/ckeditor5-core/pl', { + expect( fetchMock ).toHaveBeenNthCalledWith( 2, 'https://example.com/ckeditor5-core/pl', { + method: 'GET', headers: { Authorization: 'Bearer secretToken' } } ); - sinon.assert.calledWithExactly( stubs.fetch.thirdCall, 'https://example.com/ckeditor5-core/de', { + expect( fetchMock ).toHaveBeenNthCalledWith( 3, 'https://example.com/ckeditor5-core/de', { + method: 'GET', headers: { Authorization: 'Bearer secretToken' } } ); - expect( [ ...translations.entries() ] ).to.deep.equal( [ + expect( [ ...translations.entries() ] ).toEqual( [ [ 'en', 'ckeditor5-core-en-content' ], [ 'pl', 'ckeditor5-core-pl-content' ], [ 'de', 'ckeditor5-core-de-content' ] ] ); - expect( failedDownloads ).to.deep.equal( [] ); + expect( failedDownloads ).toEqual( [] ); } ); it( 'should return requested translations after multiple different download retries', async () => { - const clock = sinon.useFakeTimers(); + vi.useFakeTimers(); - const redirectFetch = () => Promise.resolve( { - ok: true, - redirected: false - } ); + const languageCallsBeforeResolving = { + en: 9, + pl: 4, + de: 7 + }; - const resolveFetch = url => Promise.resolve( { - ok: true, - redirected: true, - text: () => Promise.resolve( mocks.translations[ url ] ) + fetchMock.mockImplementation( url => { + const language = url.split( '/' ).pop(); + + if ( languageCallsBeforeResolving[ language ] > 0 ) { + languageCallsBeforeResolving[ language ]--; + + return Promise.resolve( { + ok: true, + redirected: false + } ); + } + + return Promise.resolve( { + ok: true, + redirected: true, + text: () => Promise.resolve( testData.translations[ url ] ) + } ); } ); - stubs.fetch - .withArgs( 'https://example.com/ckeditor5-core/en' ) - .callsFake( redirectFetch ) - .onCall( 9 ) - .callsFake( resolveFetch ); - - stubs.fetch - .withArgs( 'https://example.com/ckeditor5-core/pl' ) - .callsFake( redirectFetch ) - .onCall( 4 ) - .callsFake( resolveFetch ); - - stubs.fetch - .withArgs( 'https://example.com/ckeditor5-core/de' ) - .callsFake( redirectFetch ) - .onCall( 7 ) - .callsFake( resolveFetch ); - - const resource = mocks.resources[ 0 ]; - const languages = [ ...mocks.languages ]; + const resource = testData.resources[ 0 ]; + const languages = [ ...testData.languages ]; const translationsPromise = transifexService.getTranslations( resource, languages ); - await clock.tickAsync( 30000 ); + await vi.advanceTimersByTimeAsync( 30000 ); const { translations, failedDownloads } = await translationsPromise; - sinon.assert.callCount( stubs.fetch, 23 ); + expect( fetchMock ).toHaveBeenCalledTimes( 23 ); - expect( [ ...translations.entries() ] ).to.deep.equal( [ + expect( [ ...translations.entries() ] ).toEqual( [ [ 'en', 'ckeditor5-core-en-content' ], [ 'pl', 'ckeditor5-core-pl-content' ], [ 'de', 'ckeditor5-core-de-content' ] ] ); - expect( failedDownloads ).to.deep.equal( [] ); + expect( failedDownloads ).toEqual( [] ); } ); it( 'should return failed requests if all file downloads failed', async () => { - stubs.fetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValue( { ok: false, status: 500, statusText: 'Internal Server Error' } ); - const resource = mocks.resources[ 0 ]; - const languages = [ ...mocks.languages ]; + const resource = testData.resources[ 0 ]; + const languages = [ ...testData.languages ]; const { translations, failedDownloads } = await transifexService.getTranslations( resource, languages ); const expectedFailedDownloads = [ 'en', 'pl', 'de' ].map( languageCode => ( { @@ -388,23 +381,23 @@ describe( 'dev-transifex/transifex-service', () => { errorMessage: 'Failed to download the translation file. Received response: 500 Internal Server Error' } ) ); - expect( failedDownloads ).to.deep.equal( expectedFailedDownloads ); - expect( [ ...translations.entries() ] ).to.deep.equal( [] ); + expect( failedDownloads ).toEqual( expectedFailedDownloads ); + expect( [ ...translations.entries() ] ).toEqual( [] ); } ); it( 'should return failed requests if the retry limit has been reached for all requests', async () => { - const clock = sinon.useFakeTimers(); + vi.useFakeTimers(); - stubs.fetch.resolves( { + vi.mocked( fetchMock ).mockResolvedValue( { ok: true, redirected: false } ); - const resource = mocks.resources[ 0 ]; - const languages = [ ...mocks.languages ]; + const resource = testData.resources[ 0 ]; + const languages = [ ...testData.languages ]; const translationsPromise = transifexService.getTranslations( resource, languages ); - await clock.tickAsync( 30000 ); + await vi.advanceTimersByTimeAsync( 30000 ); const { translations, failedDownloads } = await translationsPromise; @@ -415,21 +408,21 @@ describe( 'dev-transifex/transifex-service', () => { 'Requested file is not ready yet, but the limit of file download attempts has been reached.' } ) ); - expect( failedDownloads ).to.deep.equal( expectedFailedDownloads ); - expect( [ ...translations.entries() ] ).to.deep.equal( [] ); + expect( failedDownloads ).toEqual( expectedFailedDownloads ); + expect( [ ...translations.entries() ] ).toEqual( [] ); } ); it( 'should return failed requests if it is not possible to create all initial download requests', async () => { - const clock = sinon.useFakeTimers(); + vi.useFakeTimers(); - stubs.transifexApi.ResourceStringsAsyncDownload.create.rejects(); - stubs.transifexApi.ResourceTranslationsAsyncDownload.create.rejects(); + transifexApiMock.ResourceStringsAsyncDownload.create.mockRejectedValue(); + transifexApiMock.ResourceTranslationsAsyncDownload.create.mockRejectedValue(); - const resource = mocks.resources[ 0 ]; - const languages = [ ...mocks.languages ]; + const resource = testData.resources[ 0 ]; + const languages = [ ...testData.languages ]; const translationsPromise = transifexService.getTranslations( resource, languages ); - await clock.tickAsync( 30000 ); + await vi.advanceTimersByTimeAsync( 30000 ); const { translations, failedDownloads } = await translationsPromise; @@ -439,67 +432,69 @@ describe( 'dev-transifex/transifex-service', () => { errorMessage: 'Failed to create download request.' } ) ); - expect( failedDownloads ).to.deep.equal( expectedFailedDownloads ); - expect( [ ...translations.entries() ] ).to.deep.equal( [] ); + expect( failedDownloads ).toEqual( expectedFailedDownloads ); + expect( [ ...translations.entries() ] ).toEqual( [] ); } ); it( 'should return requested translations and failed downloads in multiple different download scenarios', async () => { - const clock = sinon.useFakeTimers(); + vi.useFakeTimers(); - const redirectFetch = () => Promise.resolve( { - ok: true, - redirected: false - } ); + transifexApiMock.ResourceStringsAsyncDownload.create.mockRejectedValue(); - const resolveFetch = url => Promise.resolve( { - ok: true, - redirected: true, - text: () => Promise.resolve( mocks.translations[ url ] ) - } ); + transifexApiMock.ResourceTranslationsAsyncDownload.create + .mockRejectedValueOnce() + .mockRejectedValueOnce() + .mockRejectedValueOnce(); - const rejectFetch = () => Promise.resolve( { - ok: false, - status: 500, - statusText: 'Internal Server Error' + const languageCallsBeforeResolving = { + pl: 4, + de: 8 + }; + + fetchMock.mockImplementation( url => { + const language = url.split( '/' ).pop(); + + if ( languageCallsBeforeResolving[ language ] > 0 ) { + languageCallsBeforeResolving[ language ]--; + + return Promise.resolve( { + ok: true, + redirected: false + } ); + } + + if ( language === 'pl' ) { + return Promise.resolve( { + ok: true, + redirected: true, + text: () => Promise.resolve( testData.translations[ url ] ) + } ); + } + + if ( language === 'de' ) { + return Promise.resolve( { + ok: false, + status: 500, + statusText: 'Internal Server Error' + } ); + } } ); - stubs.transifexApi.ResourceStringsAsyncDownload.create.rejects(); - - stubs.transifexApi.ResourceTranslationsAsyncDownload.create - .onCall( 0 ) - .rejects() - .onCall( 1 ) - .rejects() - .onCall( 2 ) - .rejects(); - - stubs.fetch - .withArgs( 'https://example.com/ckeditor5-core/pl' ) - .callsFake( redirectFetch ) - .onCall( 4 ) - .callsFake( resolveFetch ); - - stubs.fetch - .withArgs( 'https://example.com/ckeditor5-core/de' ) - .callsFake( redirectFetch ) - .onCall( 8 ) - .callsFake( rejectFetch ); - - const resource = mocks.resources[ 0 ]; - const languages = [ ...mocks.languages ]; + const resource = testData.resources[ 0 ]; + const languages = [ ...testData.languages ]; const translationsPromise = transifexService.getTranslations( resource, languages ); - await clock.tickAsync( 60000 ); + await vi.advanceTimersByTimeAsync( 60000 ); const { translations, failedDownloads } = await translationsPromise; - sinon.assert.callCount( stubs.fetch, 14 ); + expect( fetchMock ).toHaveBeenCalledTimes( 14 ); - expect( [ ...translations.entries() ] ).to.deep.equal( [ + expect( [ ...translations.entries() ] ).toEqual( [ [ 'pl', 'ckeditor5-core-pl-content' ] ] ); - expect( failedDownloads ).to.deep.equal( [ + expect( failedDownloads ).toEqual( [ { resourceName: 'ckeditor5-core', languageCode: 'en', @@ -516,9 +511,9 @@ describe( 'dev-transifex/transifex-service', () => { describe( 'getResourceTranslations', () => { it( 'should return all found translations', () => { - stubs.getNextResourceTranslations = null; - stubs.fetchResourceTranslations.callsFake( () => { - stubs.dataResourceTranslations = [ + getNextResourceTranslationsMock = null; + fetchResourceTranslationsMock.mockImplementation( () => { + dataResourceTranslationsMock = [ { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:680k83DmCPu9AkGVwDvVQqCvsJkg93AC:l:en' }, { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:MbFEbBcsOk43LryccpBHPyeMYBW6G5FV:l:en' }, { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:tQ8xmNQ706zjL3hiqEsttqUoneZJtV4Q:l:en' } @@ -529,22 +524,22 @@ describe( 'dev-transifex/transifex-service', () => { return transifexService.getResourceTranslations( 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo', 'l:en' ) .then( result => { - expect( result ).to.deep.equal( [ + expect( result ).toEqual( [ { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:680k83DmCPu9AkGVwDvVQqCvsJkg93AC:l:en' }, { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:MbFEbBcsOk43LryccpBHPyeMYBW6G5FV:l:en' }, { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:tQ8xmNQ706zjL3hiqEsttqUoneZJtV4Q:l:en' } ] ); - expect( stubs.filterResourceTranslations.callCount ).to.equal( 1 ); - expect( stubs.filterResourceTranslations.firstCall.args[ 0 ] ).to.deep.equal( { + expect( filterResourceTranslationsMock ).toHaveBeenCalledTimes( 1 ); + expect( filterResourceTranslationsMock ).toHaveBeenCalledWith( { resource: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo', language: 'l:en' } ); - expect( stubs.includeResourceTranslations.callCount ).to.equal( 1 ); - expect( stubs.includeResourceTranslations.firstCall.args[ 0 ] ).to.equal( 'resource_string' ); + expect( includeResourceTranslationsMock ).toHaveBeenCalledTimes( 1 ); + expect( includeResourceTranslationsMock ).toHaveBeenCalledWith( 'resource_string' ); - expect( stubs.fetchResourceTranslations.callCount ).to.equal( 1 ); + expect( fetchResourceTranslationsMock ).toHaveBeenCalledTimes( 1 ); } ); } ); @@ -555,47 +550,47 @@ describe( 'dev-transifex/transifex-service', () => { { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:tQ8xmNQ706zjL3hiqEsttqUoneZJtV4Q:l:en' } ]; - stubs.getNextResourceTranslations.callsFake( () => { - stubs.dataResourceTranslations = [ availableTranslations.shift() ]; + getNextResourceTranslationsMock.mockImplementation( () => { + dataResourceTranslationsMock = [ availableTranslations.shift() ]; return Promise.resolve( { - data: stubs.dataResourceTranslations, + data: dataResourceTranslationsMock, next: availableTranslations.length > 0, - getNext: stubs.getNextResourceTranslations + getNext: getNextResourceTranslationsMock } ); } ); - stubs.fetchResourceTranslations.callsFake( () => { - stubs.dataResourceTranslations = [ availableTranslations.shift() ]; + fetchResourceTranslationsMock.mockImplementation( () => { + dataResourceTranslationsMock = [ availableTranslations.shift() ]; return Promise.resolve(); } ); return transifexService.getResourceTranslations( 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo', 'l:en' ) .then( result => { - expect( result ).to.deep.equal( [ + expect( result ).toEqual( [ { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:680k83DmCPu9AkGVwDvVQqCvsJkg93AC:l:en' }, { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:MbFEbBcsOk43LryccpBHPyeMYBW6G5FV:l:en' }, { id: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo:s:tQ8xmNQ706zjL3hiqEsttqUoneZJtV4Q:l:en' } ] ); - expect( stubs.filterResourceTranslations.callCount ).to.equal( 1 ); - expect( stubs.filterResourceTranslations.firstCall.args[ 0 ] ).to.deep.equal( { + expect( filterResourceTranslationsMock ).toHaveBeenCalledTimes( 1 ); + expect( filterResourceTranslationsMock ).toHaveBeenCalledWith( { resource: 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo', language: 'l:en' } ); - expect( stubs.includeResourceTranslations.callCount ).to.equal( 1 ); - expect( stubs.includeResourceTranslations.firstCall.args[ 0 ] ).to.equal( 'resource_string' ); + expect( includeResourceTranslationsMock ).toHaveBeenCalledTimes( 1 ); + expect( includeResourceTranslationsMock ).toHaveBeenCalledWith( 'resource_string' ); - expect( stubs.fetchResourceTranslations.callCount ).to.equal( 1 ); + expect( fetchResourceTranslationsMock ).toHaveBeenCalledTimes( 1 ); } ); } ); it( 'should reject a promise if Transifex API rejected', async () => { const apiError = new Error( 'JsonApiError: 418, I\'m a teapot' ); - stubs.fetchResourceTranslations.rejects( apiError ); + fetchResourceTranslationsMock.mockRejectedValue( apiError ); return transifexService.getResourceTranslations( 'o:ckeditor:p:ckeditor5:r:ckeditor5-foo', 'l:en' ) .then( @@ -603,7 +598,7 @@ describe( 'dev-transifex/transifex-service', () => { throw new Error( 'Expected to be rejected.' ); }, error => { - expect( apiError ).to.equal( error ); + expect( apiError ).toEqual( error ); } ); } ); @@ -613,7 +608,7 @@ describe( 'dev-transifex/transifex-service', () => { it( 'should extract the resource name from the resource instance', () => { const resource = { attributes: { slug: 'ckeditor5-core' } }; - expect( transifexService.getResourceName( resource ) ).to.equal( 'ckeditor5-core' ); + expect( transifexService.getResourceName( resource ) ).toEqual( 'ckeditor5-core' ); } ); } ); @@ -621,7 +616,7 @@ describe( 'dev-transifex/transifex-service', () => { it( 'should extract the language code from the language instance', () => { const language = { attributes: { code: 'pl' } }; - expect( transifexService.getLanguageCode( language ) ).to.equal( 'pl' ); + expect( transifexService.getLanguageCode( language ) ).toEqual( 'pl' ); } ); } ); @@ -629,13 +624,13 @@ describe( 'dev-transifex/transifex-service', () => { it( 'should return false if the language instance is not the source language', () => { const language = { attributes: { code: 'pl' } }; - expect( transifexService.isSourceLanguage( language ) ).to.be.false; + expect( transifexService.isSourceLanguage( language ) ).toEqual( false ); } ); it( 'should return true if the language instance is the source language', () => { const language = { attributes: { code: 'en' } }; - expect( transifexService.isSourceLanguage( language ) ).to.be.true; + expect( transifexService.isSourceLanguage( language ) ).toEqual( true ); } ); } ); @@ -668,12 +663,12 @@ describe( 'dev-transifex/transifex-service', () => { } }; - stubs.createResource.resolves( apiResponse ); + createResourceMock.mockResolvedValue( apiResponse ); return transifexService.createResource( { organizationName, projectName, resourceName } ) .then( response => { - expect( stubs.createResource.callCount ).to.equal( 1 ); - expect( stubs.createResource.firstCall.args[ 0 ] ).to.deep.equal( { + expect( createResourceMock ).toHaveBeenCalledTimes( 1 ); + expect( createResourceMock ).toHaveBeenCalledWith( { name: 'ckeditor5-foo', relationships: { i18n_format: { @@ -692,7 +687,7 @@ describe( 'dev-transifex/transifex-service', () => { slug: 'ckeditor5-foo' } ); - expect( response ).to.equal( apiResponse ); + expect( response ).toEqual( apiResponse ); } ); } ); @@ -703,7 +698,7 @@ describe( 'dev-transifex/transifex-service', () => { const apiError = new Error( 'JsonApiError: 418, I\'m a teapot' ); - stubs.createResource.rejects( apiError ); + createResourceMock.mockRejectedValue( apiError ); return transifexService.createResource( { organizationName, projectName, resourceName } ) .then( @@ -711,7 +706,7 @@ describe( 'dev-transifex/transifex-service', () => { throw new Error( 'Expected to be rejected.' ); }, err => { - expect( apiError ).to.equal( err ); + expect( apiError ).toEqual( err ); } ); } ); @@ -729,12 +724,12 @@ describe( 'dev-transifex/transifex-service', () => { type: 'resource_strings_async_uploads' }; - stubs.createResourceStringsAsyncUpload.resolves( apiResponse ); + createResourceStringsAsyncUploadMock.mockResolvedValue( apiResponse ); return transifexService.createSourceFile( { organizationName, projectName, resourceName, content } ) .then( response => { - expect( stubs.createResourceStringsAsyncUpload.callCount ).to.equal( 1 ); - expect( stubs.createResourceStringsAsyncUpload.firstCall.args[ 0 ] ).to.deep.equal( { + expect( createResourceStringsAsyncUploadMock ).toHaveBeenCalledTimes( 1 ); + expect( createResourceStringsAsyncUploadMock ).toHaveBeenCalledWith( { attributes: { content: '# ckeditor5-foo', content_encoding: 'text' @@ -750,7 +745,7 @@ describe( 'dev-transifex/transifex-service', () => { type: 'resource_strings_async_uploads' } ); - expect( response ).to.equal( apiResponse.id ); + expect( response ).toEqual( apiResponse.id ); } ); } ); @@ -762,7 +757,7 @@ describe( 'dev-transifex/transifex-service', () => { const apiError = new Error( 'JsonApiError: 418, I\'m a teapot' ); - stubs.createResourceStringsAsyncUpload.rejects( apiError ); + createResourceStringsAsyncUploadMock.mockRejectedValue( apiError ); return transifexService.createSourceFile( { organizationName, projectName, resourceName, content } ) .then( @@ -770,21 +765,15 @@ describe( 'dev-transifex/transifex-service', () => { throw new Error( 'Expected to be rejected.' ); }, err => { - expect( apiError ).to.equal( err ); + expect( apiError ).toEqual( err ); } ); } ); } ); describe( 'getResourceUploadDetails', () => { - let clock; - beforeEach( () => { - clock = sinon.useFakeTimers(); - } ); - - afterEach( () => { - clock.restore(); + vi.useFakeTimers(); } ); it( 'should return a promise with resolved upload details (Transifex processed the upload)', async () => { @@ -796,17 +785,17 @@ describe( 'dev-transifex/transifex-service', () => { type: 'resource_strings_async_uploads' }; - stubs.getResourceStringsAsyncUpload.resolves( apiResponse ); + getResourceStringsAsyncUploadMock.mockResolvedValue( apiResponse ); const promise = transifexService.getResourceUploadDetails( '4abfc726-6a27-4c33-9d99-e5254c8df748' ); - expect( promise ).to.be.a( 'promise' ); - expect( stubs.getResourceStringsAsyncUpload.callCount ).to.equal( 1 ); - expect( stubs.getResourceStringsAsyncUpload.firstCall.args[ 0 ] ).to.equal( '4abfc726-6a27-4c33-9d99-e5254c8df748' ); + expect( promise ).toBeInstanceOf( Promise ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenCalledTimes( 1 ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenCalledWith( '4abfc726-6a27-4c33-9d99-e5254c8df748' ); const result = await promise; - expect( result ).to.equal( apiResponse ); + expect( result ).toEqual( apiResponse ); } ); it( 'should return a promise that resolves after 3000ms (Transifex processed the upload 1s, status=pending)', async () => { @@ -818,22 +807,22 @@ describe( 'dev-transifex/transifex-service', () => { type: 'resource_strings_async_uploads' }; - stubs.getResourceStringsAsyncUpload.onFirstCall().resolves( { + getResourceStringsAsyncUploadMock.mockResolvedValueOnce( { attributes: { status: 'pending' } } ); - stubs.getResourceStringsAsyncUpload.onSecondCall().resolves( apiResponse ); + getResourceStringsAsyncUploadMock.mockResolvedValueOnce( apiResponse ); const promise = transifexService.getResourceUploadDetails( '4abfc726-6a27-4c33-9d99-e5254c8df748' ); - expect( promise ).to.be.a( 'promise' ); - expect( stubs.getResourceStringsAsyncUpload.callCount ).to.equal( 1 ); - expect( stubs.getResourceStringsAsyncUpload.firstCall.args[ 0 ] ).to.equal( '4abfc726-6a27-4c33-9d99-e5254c8df748' ); + expect( promise ).toBeInstanceOf( Promise ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenCalledTimes( 1 ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenNthCalledWith( 1, '4abfc726-6a27-4c33-9d99-e5254c8df748' ); - await clock.tickAsync( 3000 ); - expect( stubs.getResourceStringsAsyncUpload.callCount ).to.equal( 2 ); - expect( stubs.getResourceStringsAsyncUpload.secondCall.args[ 0 ] ).to.equal( '4abfc726-6a27-4c33-9d99-e5254c8df748' ); + await vi.advanceTimersByTimeAsync( 3000 ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenCalledTimes( 2 ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenNthCalledWith( 2, '4abfc726-6a27-4c33-9d99-e5254c8df748' ); - expect( await promise ).to.equal( apiResponse ); + expect( await promise ).toEqual( apiResponse ); } ); it( 'should return a promise that resolves after 3000ms (Transifex processed the upload 1s, status=processing)', async () => { @@ -845,28 +834,28 @@ describe( 'dev-transifex/transifex-service', () => { type: 'resource_strings_async_uploads' }; - stubs.getResourceStringsAsyncUpload.onFirstCall().resolves( { + getResourceStringsAsyncUploadMock.mockResolvedValueOnce( { attributes: { status: 'processing' } } ); - stubs.getResourceStringsAsyncUpload.onSecondCall().resolves( apiResponse ); + getResourceStringsAsyncUploadMock.mockResolvedValueOnce( apiResponse ); const promise = transifexService.getResourceUploadDetails( '4abfc726-6a27-4c33-9d99-e5254c8df748' ); - expect( promise ).to.be.a( 'promise' ); - expect( stubs.getResourceStringsAsyncUpload.callCount ).to.equal( 1 ); - expect( stubs.getResourceStringsAsyncUpload.firstCall.args[ 0 ] ).to.equal( '4abfc726-6a27-4c33-9d99-e5254c8df748' ); + expect( promise ).toBeInstanceOf( Promise ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenCalledTimes( 1 ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenNthCalledWith( 1, '4abfc726-6a27-4c33-9d99-e5254c8df748' ); - await clock.tickAsync( 3000 ); - expect( stubs.getResourceStringsAsyncUpload.callCount ).to.equal( 2 ); - expect( stubs.getResourceStringsAsyncUpload.secondCall.args[ 0 ] ).to.equal( '4abfc726-6a27-4c33-9d99-e5254c8df748' ); + await vi.advanceTimersByTimeAsync( 3000 ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenCalledTimes( 2 ); + expect( getResourceStringsAsyncUploadMock ).toHaveBeenNthCalledWith( 2, '4abfc726-6a27-4c33-9d99-e5254c8df748' ); - expect( await promise ).to.equal( apiResponse ); + expect( await promise ).toEqual( apiResponse ); } ); it( 'should return a promise that rejects if Transifex returned an error (no-delay)', async () => { const apiResponse = new Error( 'JsonApiError' ); - stubs.getResourceStringsAsyncUpload.rejects( apiResponse ); + getResourceStringsAsyncUploadMock.mockRejectedValue( apiResponse ); return transifexService.getResourceUploadDetails( '4abfc726-6a27-4c33-9d99-e5254c8df748' ) .then( @@ -874,7 +863,7 @@ describe( 'dev-transifex/transifex-service', () => { throw new Error( 'Expected to be rejected.' ); }, err => { - expect( err ).to.equal( apiResponse ); + expect( err ).toEqual( apiResponse ); } ); } ); @@ -882,10 +871,10 @@ describe( 'dev-transifex/transifex-service', () => { it( 'should return a promise that rejects if Transifex returned an error (delay)', async () => { const apiResponse = new Error( 'JsonApiError' ); - stubs.getResourceStringsAsyncUpload.onFirstCall().resolves( { + getResourceStringsAsyncUploadMock.mockResolvedValueOnce( { attributes: { status: 'processing' } } ); - stubs.getResourceStringsAsyncUpload.onSecondCall().rejects( apiResponse ); + getResourceStringsAsyncUploadMock.mockRejectedValueOnce( apiResponse ); const promise = transifexService.getResourceUploadDetails( '4abfc726-6a27-4c33-9d99-e5254c8df748' ) .then( @@ -893,13 +882,13 @@ describe( 'dev-transifex/transifex-service', () => { throw new Error( 'Expected to be rejected.' ); }, err => { - expect( err ).to.equal( apiResponse ); + expect( err ).toEqual( apiResponse ); } ); - expect( promise ).to.be.a( 'promise' ); + expect( promise ).toBeInstanceOf( Promise ); - await clock.tickAsync( 3000 ); + await vi.advanceTimersByTimeAsync( 3000 ); return promise; } ); @@ -907,7 +896,7 @@ describe( 'dev-transifex/transifex-service', () => { it( 'should return a promise that rejects if reached the maximum number of requests to Transifex', async () => { // 10 is equal to the `MAX_REQUEST_ATTEMPTS` constant. for ( let i = 0; i < 10; ++i ) { - stubs.getResourceStringsAsyncUpload.onCall( i ).resolves( { + getResourceStringsAsyncUploadMock.mockResolvedValueOnce( { attributes: { status: 'processing' } } ); } @@ -918,7 +907,7 @@ describe( 'dev-transifex/transifex-service', () => { throw new Error( 'Expected to be rejected.' ); }, err => { - expect( err ).to.deep.equal( { + expect( err ).toEqual( { errors: [ { detail: 'Failed to retrieve the upload details.' @@ -928,17 +917,35 @@ describe( 'dev-transifex/transifex-service', () => { } ); - expect( promise ).to.be.a( 'promise' ); + expect( promise ).toBeInstanceOf( Promise ); for ( let i = 0; i < 9; ++i ) { expect( - stubs.getResourceStringsAsyncUpload.callCount, `getResourceStringsAsyncUpload, call: ${ i + 1 }` - ).to.equal( i + 1 ); + getResourceStringsAsyncUploadMock, `getResourceStringsAsyncUpload, call: ${ i + 1 }` + ).toHaveBeenCalledTimes( i + 1 ); - await clock.tickAsync( 3000 ); + await vi.advanceTimersByTimeAsync( 3000 ); } return promise; } ); } ); } ); + +function resourceAsyncDownloadMockFactory() { + return { + create: vi.fn().mockImplementation( ( { attributes, relationships, type } ) => { + const resourceName = relationships.resource.attributes.slug; + const languageCode = relationships.language ? relationships.language.attributes.code : 'en'; + + return Promise.resolve( { + attributes, + type, + links: { + self: `https://example.com/${ resourceName }/${ languageCode }` + }, + related: relationships + } ); + } ) + }; +} diff --git a/packages/ckeditor5-dev-transifex/tests/upload.js b/packages/ckeditor5-dev-transifex/tests/upload.js index 4bcac4715..65995558a 100644 --- a/packages/ckeditor5-dev-transifex/tests/upload.js +++ b/packages/ckeditor5-dev-transifex/tests/upload.js @@ -3,123 +3,94 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const sinon = require( 'sinon' ); -const { expect } = require( 'chai' ); -const proxyquire = require( 'proxyquire' ); -const mockery = require( 'mockery' ); - -describe( 'dev-transifex/upload()', () => { - let stubs, upload; +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import upload from '../lib/upload.js'; + +import { tools } from '@ckeditor/ckeditor5-dev-utils'; +import { verifyProperties, createLogger } from '../lib/utils.js'; +import chalk from 'chalk'; +import fs from 'fs/promises'; +import path from 'path'; +import transifexService from '../lib/transifexservice.js'; + +const { + tableConstructorSpy, + tablePushMock, + tableToStringMock +} = vi.hoisted( () => { + return { + tableConstructorSpy: vi.fn(), + tablePushMock: vi.fn(), + tableToStringMock: vi.fn() + }; +} ); - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); +vi.mock( '../lib/transifexservice.js' ); +vi.mock( '../lib/utils.js' ); +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( 'fs/promises' ); +vi.mock( 'path' ); + +vi.mock( 'chalk', () => ( { + default: { + cyan: vi.fn( string => string ), + gray: vi.fn( string => string ), + italic: vi.fn( string => string ), + underline: vi.fn( string => string ) + } +} ) ); + +vi.mock( 'cli-table', () => { + return { + default: class { + constructor( ...args ) { + tableConstructorSpy( ...args ); - stubs = { - fs: { - readFile: sinon.stub(), - writeFile: sinon.stub(), - lstat: sinon.stub(), - unlink: sinon.stub() - }, - - path: { - join: sinon.stub().callsFake( ( ...chunks ) => chunks.join( '/' ) ) - }, - - logger: { - progress: sinon.stub(), - info: sinon.stub(), - warning: sinon.stub(), - error: sinon.stub(), - _log: sinon.stub() - }, - - transifexService: { - init: sinon.stub(), - getProjectData: sinon.stub(), - createResource: sinon.stub(), - createSourceFile: sinon.stub(), - getResourceUploadDetails: sinon.stub() - }, - - table: { - constructor: sinon.stub(), - push: sinon.stub(), - toString: sinon.stub() - }, - - tools: { - createSpinner: sinon.stub() - }, - - chalk: { - gray: sinon.stub().callsFake( msg => msg ), - cyan: sinon.stub().callsFake( msg => msg ), - italic: sinon.stub().callsFake( msg => msg ), - underline: sinon.stub().callsFake( msg => msg ) - }, - - utils: { - verifyProperties: sinon.stub(), - createLogger: sinon.stub() + this.push = tablePushMock; + this.toString = tableToStringMock; } - }; - - stubs.utils.createLogger.returns( { - progress: stubs.logger.progress, - info: stubs.logger.info, - warning: stubs.logger.warning, - error: stubs.logger.error, - _log: stubs.logger._log - } ); - - // `proxyquire` does not understand dynamic imports. - mockery.registerMock( '/home/ckeditor5-with-errors/.transifex-failed-uploads.json', { - 'ckeditor5-non-existing-01': [ - 'Resource with this Slug and Project already exists.' - ], - 'ckeditor5-non-existing-02': [ - 'Object not found. It may have been deleted or not been created yet.' - ] - } ); + } + }; +} ); - upload = proxyquire( '../lib/upload', { - '@ckeditor/ckeditor5-dev-utils': { - tools: stubs.tools - }, - 'path': stubs.path, - 'fs/promises': stubs.fs, - 'chalk': stubs.chalk, - 'cli-table': class Table { - constructor( ...args ) { - stubs.table.constructor( ...args ); - } +vi.mock( '/home/ckeditor5-with-errors/.transifex-failed-uploads.json', () => ( { + default: { + 'ckeditor5-non-existing-01': [ + 'Resource with this Slug and Project already exists.' + ], + 'ckeditor5-non-existing-02': [ + 'Object not found. It may have been deleted or not been created yet.' + ] + } +} ) ); - push( ...args ) { - return stubs.table.push( ...args ); - } +describe( 'dev-transifex/upload()', () => { + let loggerProgressMock, loggerInfoMock, loggerWarningMock, loggerErrorMock, loggerLogMock; - toString( ...args ) { - return stubs.table.toString( ...args ); - } - }, - './transifexservice': stubs.transifexService, - './utils': stubs.utils + beforeEach( () => { + vi.mocked( path.join ).mockImplementation( ( ...args ) => args.join( '/' ) ); + + loggerProgressMock = vi.fn(); + loggerInfoMock = vi.fn(); + loggerWarningMock = vi.fn(); + loggerErrorMock = vi.fn(); + loggerErrorMock = vi.fn(); + + vi.mocked( fs.lstat ).mockRejectedValue(); + + vi.mocked( createLogger ).mockImplementation( () => { + return { + progress: loggerProgressMock, + info: loggerInfoMock, + warning: loggerWarningMock, + error: loggerErrorMock, + _log: loggerLogMock + }; } ); - - stubs.fs.lstat.withArgs( '/home/ckeditor5/.transifex-failed-uploads.json' ).rejects(); } ); afterEach( () => { - sinon.restore(); - mockery.deregisterAll(); - mockery.disable(); + vi.resetAllMocks(); } ); it( 'should reject a promise if required properties are not specified', () => { @@ -131,35 +102,32 @@ describe( 'dev-transifex/upload()', () => { projectName: 'ckeditor5' }; - stubs.utils.verifyProperties.throws( error ); + vi.mocked( verifyProperties ).mockImplementation( () => { + throw new Error( error ); + } ); return upload( config ) .then( () => { throw new Error( 'Expected to be rejected.' ); }, - err => { - expect( err ).to.equal( error ); - - expect( stubs.utils.verifyProperties.callCount ).to.equal( 1 ); - expect( stubs.utils.verifyProperties.firstCall.args[ 0 ] ).to.deep.equal( config ); - expect( stubs.utils.verifyProperties.firstCall.args[ 1 ] ).to.deep.equal( [ - 'token', - 'organizationName', - 'projectName', - 'cwd', - 'packages' - ] ); + caughtError => { + expect( caughtError.message.endsWith( error.message ) ).toEqual( true ); + + expect( vi.mocked( verifyProperties ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( verifyProperties ) ).toHaveBeenCalledWith( + config, [ 'token', 'organizationName', 'projectName', 'cwd', 'packages' ] + ); } ); } ); - it( 'should store an error log if cannot find the project details', () => { + it( 'should store an error log if cannot find the project details', async () => { const packages = new Map( [ [ 'ckeditor5-existing-11', 'build/.transifex/ckeditor5-existing-11' ] ] ); - stubs.transifexService.getProjectData.rejects( new Error( 'Invalid auth' ) ); + vi.mocked( transifexService.getProjectData ).mockRejectedValue( new Error( 'Invalid auth' ) ); const config = { packages, @@ -169,24 +137,25 @@ describe( 'dev-transifex/upload()', () => { projectName: 'ckeditor5' }; - return upload( config ) - .then( () => { - expect( stubs.logger.error.callCount ).to.equal( 2 ); - expect( stubs.logger.error.firstCall.args[ 0 ] ).to.equal( 'Cannot find project details for "ckeditor/ckeditor5".' ); - expect( stubs.logger.error.secondCall.args[ 0 ] ).to.equal( - 'Make sure you specified a valid auth token or an organization/project names.' - ); - - expect( stubs.transifexService.getProjectData.callCount ).to.equal( 1 ); - expect( stubs.transifexService.getProjectData.firstCall.args[ 0 ] ).to.equal( 'ckeditor' ); - expect( stubs.transifexService.getProjectData.firstCall.args[ 1 ] ).to.equal( 'ckeditor5' ); - expect( stubs.transifexService.getProjectData.firstCall.args[ 2 ] ).to.deep.equal( [ ...packages.keys() ] ); - - expect( stubs.transifexService.createResource.callCount ).to.equal( 0 ); - } ); + await upload( config ); + + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenNthCalledWith( + 1, 'Cannot find project details for "ckeditor/ckeditor5".' + ); + expect( vi.mocked( loggerErrorMock ) ).toHaveBeenNthCalledWith( + 2, 'Make sure you specified a valid auth token or an organization/project names.' + ); + + expect( vi.mocked( transifexService.getProjectData ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( transifexService.getProjectData ) ).toHaveBeenCalledWith( + 'ckeditor', 'ckeditor5', [ ...packages.keys() ] + ); + + expect( vi.mocked( transifexService.createResource ) ).toHaveBeenCalledTimes( 0 ); } ); - it( 'should create a new resource if the package is processed for the first time', () => { + it( 'should create a new resource if the package is processed for the first time', async () => { const packages = new Map( [ [ 'ckeditor5-non-existing-01', 'build/.transifex/ckeditor5-non-existing-01' ] ] ); @@ -199,33 +168,29 @@ describe( 'dev-transifex/upload()', () => { projectName: 'ckeditor5' }; - stubs.transifexService.getProjectData.resolves( { - resources: [] - } ); - - stubs.transifexService.createResource.resolves(); - stubs.transifexService.createSourceFile.resolves( 'uuid-01' ); - stubs.transifexService.getResourceUploadDetails.resolves( + vi.mocked( transifexService.getProjectData ).mockResolvedValue( { resources: [] } ); + vi.mocked( transifexService.createResource ).mockResolvedValue(); + vi.mocked( transifexService.createSourceFile ).mockResolvedValue( 'uuid-01' ); + vi.mocked( transifexService.getResourceUploadDetails ).mockResolvedValue( createResourceUploadDetailsResponse( 'ckeditor5-non-existing-01', 0, 0, 0 ) ); - stubs.tools.createSpinner.returns( { - start: sinon.stub(), - finish: sinon.stub() + vi.mocked( tools.createSpinner ).mockReturnValue( { + start: vi.fn(), + finish: vi.fn() } ); - return upload( config ) - .then( () => { - expect( stubs.transifexService.createResource.callCount ).to.equal( 1 ); - expect( stubs.transifexService.createResource.firstCall.args[ 0 ] ).to.deep.equal( { - organizationName: 'ckeditor', - projectName: 'ckeditor5', - resourceName: 'ckeditor5-non-existing-01' - } ); - } ); + await upload( config ); + + expect( vi.mocked( transifexService.createResource ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( transifexService.createResource ) ).toHaveBeenCalledWith( { + organizationName: 'ckeditor', + projectName: 'ckeditor5', + resourceName: 'ckeditor5-non-existing-01' + } ); } ); - it( 'should not create a new resource if the package exists on Transifex', () => { + it( 'should not create a new resource if the package exists on Transifex', async () => { const packages = new Map( [ [ 'ckeditor5-existing-11', 'build/.transifex/ckeditor5-existing-11' ] ] ); @@ -238,30 +203,29 @@ describe( 'dev-transifex/upload()', () => { projectName: 'ckeditor5' }; - stubs.transifexService.getProjectData.resolves( { + vi.mocked( transifexService.getProjectData ).mockResolvedValue( { resources: [ { attributes: { name: 'ckeditor5-existing-11' } } ] } ); - stubs.transifexService.createSourceFile.resolves( 'uuid-11' ); + vi.mocked( transifexService.createSourceFile ).mockResolvedValue( 'uuid-11' ); - stubs.transifexService.getResourceUploadDetails.resolves( + vi.mocked( transifexService.getResourceUploadDetails ).mockResolvedValue( createResourceUploadDetailsResponse( 'ckeditor5-existing-11', 0, 0, 0 ) ); - stubs.tools.createSpinner.returns( { - start: sinon.stub(), - finish: sinon.stub() + vi.mocked( tools.createSpinner ).mockReturnValue( { + start: vi.fn(), + finish: vi.fn() } ); - return upload( config ) - .then( () => { - expect( stubs.transifexService.createResource.callCount ).to.equal( 0 ); - } ); + await upload( config ); + + expect( vi.mocked( transifexService.createResource ) ).toHaveBeenCalledTimes( 0 ); } ); - it( 'should send a new translation source to Transifex', () => { + it( 'should send a new translation source to Transifex', async () => { const packages = new Map( [ [ 'ckeditor5-existing-11', 'build/.transifex/ckeditor5-existing-11' ] ] ); @@ -274,43 +238,45 @@ describe( 'dev-transifex/upload()', () => { projectName: 'ckeditor5' }; - stubs.transifexService.getProjectData.resolves( { + vi.mocked( transifexService.getProjectData ).mockResolvedValue( { resources: [ { attributes: { name: 'ckeditor5-existing-11' } } ] } ); - stubs.transifexService.createSourceFile.resolves( 'uuid-11' ); + vi.mocked( transifexService.createSourceFile ).mockResolvedValue( 'uuid-11' ); - stubs.transifexService.getResourceUploadDetails.resolves( + vi.mocked( transifexService.getResourceUploadDetails ).mockResolvedValue( createResourceUploadDetailsResponse( 'ckeditor5-existing-11', 0, 0, 0 ) ); - stubs.fs.readFile.resolves( '# Example file.' ); + vi.mocked( fs.readFile ).mockResolvedValue( '# Example file.' ); - stubs.tools.createSpinner.returns( { - start: sinon.stub(), - finish: sinon.stub() + vi.mocked( tools.createSpinner ).mockReturnValue( { + start: vi.fn(), + finish: vi.fn() } ); - return upload( config ) - .then( () => { - expect( stubs.fs.readFile.callCount ).to.equal( 1 ); - expect( stubs.fs.readFile.firstCall.args[ 0 ] ).to.equal( '/home/ckeditor5/build/.transifex/ckeditor5-existing-11/en.pot' ); - expect( stubs.fs.readFile.firstCall.args[ 1 ] ).to.equal( 'utf-8' ); - expect( stubs.transifexService.createSourceFile.callCount ).to.equal( 1 ); - expect( stubs.transifexService.createSourceFile.firstCall.args[ 0 ] ).to.deep.equal( { - organizationName: 'ckeditor', - projectName: 'ckeditor5', - resourceName: 'ckeditor5-existing-11', - content: '# Example file.' - } ); - expect( stubs.transifexService.getResourceUploadDetails.callCount ).to.equal( 1 ); - expect( stubs.transifexService.getResourceUploadDetails.firstCall.args[ 0 ] ).to.equal( 'uuid-11' ); - } ); + await upload( config ); + + expect( vi.mocked( fs.readFile ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.readFile ) ).toHaveBeenCalledWith( + '/home/ckeditor5/build/.transifex/ckeditor5-existing-11/en.pot', 'utf-8' + ); + + expect( vi.mocked( transifexService.createSourceFile ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( transifexService.createSourceFile ) ).toHaveBeenCalledWith( { + organizationName: 'ckeditor', + projectName: 'ckeditor5', + resourceName: 'ckeditor5-existing-11', + content: '# Example file.' + } ); + + expect( vi.mocked( transifexService.getResourceUploadDetails ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( transifexService.getResourceUploadDetails ) ).toHaveBeenCalledWith( 'uuid-11' ); } ); - it( 'should keep informed a developer what the script does', () => { + it( 'should keep informed a developer what the script does', async () => { const packages = new Map( [ [ 'ckeditor5-non-existing-01', 'build/.transifex/ckeditor5-non-existing-01' ] ] ); @@ -323,57 +289,55 @@ describe( 'dev-transifex/upload()', () => { projectName: 'ckeditor5' }; - stubs.transifexService.getProjectData.resolves( { + vi.mocked( transifexService.getProjectData ).mockResolvedValue( { resources: [] } ); - stubs.transifexService.createResource.resolves(); - stubs.transifexService.createSourceFile.resolves( 'uuid-01' ); - stubs.transifexService.getResourceUploadDetails.resolves( + vi.mocked( transifexService.createResource ).mockResolvedValue(); + vi.mocked( transifexService.createSourceFile ).mockResolvedValue( 'uuid-01' ); + vi.mocked( transifexService.getResourceUploadDetails ).mockResolvedValue( createResourceUploadDetailsResponse( 'ckeditor5-non-existing-01', 0, 0, 0 ) ); const packageSpinner = { - start: sinon.stub(), - finish: sinon.stub() + start: vi.fn(), + finish: vi.fn() }; const processSpinner = { - start: sinon.stub(), - finish: sinon.stub() + start: vi.fn(), + finish: vi.fn() }; - stubs.tools.createSpinner.onFirstCall().returns( packageSpinner ); - stubs.tools.createSpinner.onSecondCall().returns( processSpinner ); + vi.mocked( tools.createSpinner ).mockReturnValueOnce( packageSpinner ); + vi.mocked( tools.createSpinner ).mockReturnValueOnce( processSpinner ); - stubs.table.toString.returns( '┻━┻' ); + vi.mocked( tableToStringMock ).mockReturnValue( '┻━┻' ); - return upload( config ) - .then( () => { - expect( stubs.logger.info.callCount ).to.equal( 1 ); - expect( stubs.logger.info.getCall( 0 ).args[ 0 ] ).to.equal( '┻━┻' ); - - expect( stubs.logger.progress.callCount ).to.equal( 4 ); - expect( stubs.logger.progress.getCall( 0 ).args[ 0 ] ).to.equal( 'Fetching project information...' ); - expect( stubs.logger.progress.getCall( 1 ).args[ 0 ] ).to.equal( 'Uploading new translations...' ); - expect( stubs.logger.progress.getCall( 2 ).args[ 0 ] ).to.be.undefined; - expect( stubs.logger.progress.getCall( 3 ).args[ 0 ] ).to.equal( 'Done.' ); - - expect( stubs.tools.createSpinner.callCount ).to.equal( 2 ); - - expect( stubs.tools.createSpinner.firstCall.args[ 0 ] ).to.equal( 'Processing "ckeditor5-non-existing-01"' ); - expect( stubs.tools.createSpinner.firstCall.args[ 1 ] ).to.deep.equal( { - emoji: '👉', - indentLevel: 1 - } ); - expect( stubs.tools.createSpinner.secondCall.args[ 0 ] ).to.equal( 'Collecting responses... It takes a while.' ); - - expect( packageSpinner.start.called ).to.equal( true ); - expect( packageSpinner.finish.called ).to.equal( true ); - expect( processSpinner.start.called ).to.equal( true ); - expect( processSpinner.finish.called ).to.equal( true ); - expect( stubs.chalk.gray.callCount ).to.equal( 1 ); - expect( stubs.chalk.italic.callCount ).to.equal( 1 ); - } ); + await upload( config ); + + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( loggerInfoMock ) ).toHaveBeenCalledWith( '┻━┻' ); + + expect( vi.mocked( loggerProgressMock ) ).toHaveBeenCalledTimes( 4 ); + expect( vi.mocked( loggerProgressMock ) ).toHaveBeenNthCalledWith( 1, 'Fetching project information...' ); + expect( vi.mocked( loggerProgressMock ) ).toHaveBeenNthCalledWith( 2, 'Uploading new translations...' ); + expect( vi.mocked( loggerProgressMock ) ).toHaveBeenNthCalledWith( 3 ); + expect( vi.mocked( loggerProgressMock ) ).toHaveBeenNthCalledWith( 4, 'Done.' ); + + expect( vi.mocked( tools.createSpinner ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( tools.createSpinner ) ).toHaveBeenNthCalledWith( + 1, 'Processing "ckeditor5-non-existing-01"', { emoji: '👉', indentLevel: 1 } + ); + expect( vi.mocked( tools.createSpinner ) ).toHaveBeenNthCalledWith( + 2, 'Collecting responses... It takes a while.' + ); + + expect( packageSpinner.start ).toHaveBeenCalled(); + expect( packageSpinner.finish ).toHaveBeenCalled(); + expect( processSpinner.start ).toHaveBeenCalled(); + expect( processSpinner.finish ).toHaveBeenCalled(); + expect( vi.mocked( chalk.gray ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( chalk.italic ) ).toHaveBeenCalledTimes( 1 ); } ); describe( 'error handling', () => { @@ -395,81 +359,91 @@ describe( 'dev-transifex/upload()', () => { projectName: 'ckeditor5' }; - stubs.fs.lstat.withArgs( '/home/ckeditor5-with-errors/.transifex-failed-uploads.json' ).resolves(); + vi.mocked( fs.lstat ).mockResolvedValueOnce(); - stubs.transifexService.getProjectData.resolves( { + vi.mocked( transifexService.getProjectData ).mockResolvedValue( { resources: [] } ); - stubs.transifexService.createResource.resolves(); + vi.mocked( transifexService.createResource ).mockResolvedValue(); - // Mock Tx response when uploading a new translation content. - stubs.transifexService.createSourceFile.withArgs( { - organizationName: 'ckeditor', - projectName: 'ckeditor5', - resourceName: 'ckeditor5-non-existing-01', - content: '# ckeditor5-non-existing-01' - } ).resolves( 'uuid-01' ); - stubs.transifexService.createSourceFile.withArgs( { - organizationName: 'ckeditor', - projectName: 'ckeditor5', - resourceName: 'ckeditor5-non-existing-02', - content: '# ckeditor5-non-existing-02' - } ).resolves( 'uuid-02' ); - - // Mock translation sources. - stubs.fs.readFile.withArgs( config.cwd + '/build/.transifex/ckeditor5-non-existing-01/en.pot' ) - .resolves( '# ckeditor5-non-existing-01' ); - stubs.fs.readFile.withArgs( config.cwd + '/build/.transifex/ckeditor5-non-existing-02/en.pot' ) - .resolves( '# ckeditor5-non-existing-02' ); - - // Mock upload results. - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-01' ).resolves( - createResourceUploadDetailsResponse( 'ckeditor5-non-existing-01', 3, 0, 0 ) - ); - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-02' ).resolves( - createResourceUploadDetailsResponse( 'ckeditor5-non-existing-02', 0, 0, 0 ) - ); + vi.mocked( transifexService.createSourceFile ).mockImplementation( options => { + if ( options.resourceName === 'ckeditor5-non-existing-01' ) { + return Promise.resolve( 'uuid-01' ); + } + + if ( options.resourceName === 'ckeditor5-non-existing-02' ) { + return Promise.resolve( 'uuid-02' ); + } - stubs.tools.createSpinner.returns( { - start: sinon.stub(), - finish: sinon.stub() + return Promise.reject( { errors: [] } ); } ); - } ); - it( 'should process packages specified in the ".transifex-failed-uploads.json" file', () => { - return upload( config ) - .then( () => { - expect( stubs.logger.warning.callCount ).to.equal( 2 ); - expect( stubs.logger.warning.firstCall.args[ 0 ] ).to.equal( - 'Found the file containing a list of packages that failed during the last script execution.' + vi.mocked( fs.readFile ).mockImplementation( path => { + if ( path === config.cwd + '/build/.transifex/ckeditor5-non-existing-01/en.pot' ) { + return Promise.resolve( '# ckeditor5-non-existing-01' ); + } + + if ( path === config.cwd + '/build/.transifex/ckeditor5-non-existing-02/en.pot' ) { + return Promise.resolve( '# ckeditor5-non-existing-02' ); + } + + return Promise.resolve( '' ); + } ); + + vi.mocked( transifexService.getResourceUploadDetails ).mockImplementation( id => { + if ( id === 'uuid-01' ) { + return Promise.resolve( + createResourceUploadDetailsResponse( 'ckeditor5-non-existing-01', 3, 0, 0 ) ); - expect( stubs.logger.warning.secondCall.args[ 0 ] ).to.equal( - 'The script will process only packages listed in the file instead of all passed as "config.packages".' + } + + if ( id === 'uuid-02' ) { + return Promise.resolve( + createResourceUploadDetailsResponse( 'ckeditor5-non-existing-02', 0, 0, 0 ) ); + } + + return Promise.reject(); + } ); + + vi.mocked( tools.createSpinner ).mockReturnValue( { + start: vi.fn(), + finish: vi.fn() + } ); + } ); + + it( 'should process packages specified in the ".transifex-failed-uploads.json" file', async () => { + await upload( config ); + + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 1, 'Found the file containing a list of packages that failed during the last script execution.' + ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 2, 'The script will process only packages listed in the file instead of all passed as "config.packages".' + ); - expect( stubs.fs.readFile.callCount ).to.equal( 2 ); - expect( stubs.transifexService.createResource.callCount ).to.equal( 2 ); - expect( stubs.transifexService.createSourceFile.callCount ).to.equal( 2 ); - expect( stubs.transifexService.getResourceUploadDetails.callCount ).to.equal( 2 ); - } ); + expect( vi.mocked( fs.readFile ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( transifexService.createResource ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( transifexService.createSourceFile ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( transifexService.getResourceUploadDetails ) ).toHaveBeenCalledTimes( 2 ); } ); - it( 'should remove the ".transifex-failed-uploads.json" file if finished with no errors', () => { - return upload( config ) - .then( () => { - expect( stubs.fs.unlink.callCount ).to.equal( 1 ); - expect( stubs.fs.unlink.firstCall.args[ 0 ] ).to.equal( '/home/ckeditor5-with-errors/.transifex-failed-uploads.json' ); - } ); + it( 'should remove the ".transifex-failed-uploads.json" file if finished with no errors', async () => { + await upload( config ); + + expect( vi.mocked( fs.unlink ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.unlink ) ).toHaveBeenCalledWith( '/home/ckeditor5-with-errors/.transifex-failed-uploads.json' ); } ); - it( 'should store an error in the ".transifex-failed-uploads.json" file (cannot create a resource)', () => { + it( 'should store an error in the ".transifex-failed-uploads.json" file (cannot create a resource)', async () => { const firstSpinner = { - start: sinon.stub(), - finish: sinon.stub() + start: vi.fn(), + finish: vi.fn() }; - stubs.tools.createSpinner.onFirstCall().returns( firstSpinner ); + vi.mocked( tools.createSpinner ).mockReturnValueOnce( firstSpinner ); const error = { message: 'JsonApiError: 409', @@ -480,44 +454,42 @@ describe( 'dev-transifex/upload()', () => { ] }; - stubs.transifexService.createResource.onFirstCall().rejects( error ); - stubs.transifexService.createResource.onSecondCall().resolves(); + vi.mocked( transifexService.createResource ).mockRejectedValueOnce( error ); + vi.mocked( transifexService.createResource ).mockResolvedValueOnce(); - return upload( config ) - .then( () => { - expect( stubs.logger.warning.callCount ).to.equal( 5 ); - expect( stubs.logger.warning.getCall( 2 ).args[ 0 ] ).to.equal( - 'Not all translations were uploaded due to errors in Transifex API.' - ); - expect( stubs.logger.warning.getCall( 3 ).args[ 0 ] ).to.equal( - 'Review the "/home/ckeditor5-with-errors/.transifex-failed-uploads.json" file for more details.' - ); - expect( stubs.logger.warning.getCall( 4 ).args[ 0 ] ).to.equal( - 'Re-running the script will process only packages specified in the file.' - ); + await upload( config ); - expect( firstSpinner.finish.callCount ).to.equal( 1 ); - expect( firstSpinner.finish.firstCall.args[ 0 ] ).to.deep.equal( { emoji: '❌' } ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenCalledTimes( 5 ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 3, 'Not all translations were uploaded due to errors in Transifex API.' + ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 4, 'Review the "/home/ckeditor5-with-errors/.transifex-failed-uploads.json" file for more details.' + ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 5, 'Re-running the script will process only packages specified in the file.' + ); - expect( stubs.fs.writeFile.callCount ).to.equal( 1 ); - expect( stubs.fs.writeFile.firstCall.args[ 0 ] ).to.equal( - '/home/ckeditor5-with-errors/.transifex-failed-uploads.json' - ); + expect( firstSpinner.finish ).toHaveBeenCalledTimes( 1 ); + expect( firstSpinner.finish ).toHaveBeenCalledWith( { emoji: '❌' } ); - const storedErrors = JSON.parse( stubs.fs.writeFile.firstCall.args[ 1 ] ); - expect( storedErrors ).to.deep.equal( { - 'ckeditor5-non-existing-01': [ 'Resource with this Slug and Project already exists.' ] - } ); - } ); + expect( vi.mocked( fs.writeFile ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.writeFile ) ).toHaveBeenCalledWith( + '/home/ckeditor5-with-errors/.transifex-failed-uploads.json', + JSON.stringify( { + 'ckeditor5-non-existing-01': [ 'Resource with this Slug and Project already exists.' ] + }, null, 2 ) + '\n', + 'utf-8' + ); } ); - it( 'should store an error in the ".transifex-failed-uploads.json" file (cannot upload a translation)', () => { + it( 'should store an error in the ".transifex-failed-uploads.json" file (cannot upload a translation)', async () => { const firstSpinner = { - start: sinon.stub(), - finish: sinon.stub() + start: vi.fn(), + finish: vi.fn() }; - stubs.tools.createSpinner.onFirstCall().returns( firstSpinner ); + vi.mocked( tools.createSpinner ).mockReturnValueOnce( firstSpinner ); const error = { message: 'JsonApiError: 409', @@ -528,42 +500,35 @@ describe( 'dev-transifex/upload()', () => { ] }; - stubs.transifexService.createSourceFile.withArgs( { - organizationName: 'ckeditor', - projectName: 'ckeditor5', - resourceName: 'ckeditor5-non-existing-01', - content: '# ckeditor5-non-existing-01' - } ).rejects( error ); - - return upload( config ) - .then( () => { - expect( stubs.logger.warning.callCount ).to.equal( 5 ); - expect( stubs.logger.warning.getCall( 2 ).args[ 0 ] ).to.equal( - 'Not all translations were uploaded due to errors in Transifex API.' - ); - expect( stubs.logger.warning.getCall( 3 ).args[ 0 ] ).to.equal( - 'Review the "/home/ckeditor5-with-errors/.transifex-failed-uploads.json" file for more details.' - ); - expect( stubs.logger.warning.getCall( 4 ).args[ 0 ] ).to.equal( - 'Re-running the script will process only packages specified in the file.' - ); + vi.mocked( transifexService.createSourceFile ).mockRejectedValueOnce( error ); - expect( firstSpinner.finish.callCount ).to.equal( 1 ); - expect( firstSpinner.finish.firstCall.args[ 0 ] ).to.deep.equal( { emoji: '❌' } ); + await upload( config ); - expect( stubs.fs.writeFile.callCount ).to.equal( 1 ); - expect( stubs.fs.writeFile.firstCall.args[ 0 ] ).to.equal( - '/home/ckeditor5-with-errors/.transifex-failed-uploads.json' - ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenCalledTimes( 5 ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 3, 'Not all translations were uploaded due to errors in Transifex API.' + ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 4, 'Review the "/home/ckeditor5-with-errors/.transifex-failed-uploads.json" file for more details.' + ); + expect( vi.mocked( loggerWarningMock ) ).toHaveBeenNthCalledWith( + 5, 'Re-running the script will process only packages specified in the file.' + ); + + expect( vi.mocked( fs.writeFile ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.writeFile ) ).toHaveBeenCalledWith( + '/home/ckeditor5-with-errors/.transifex-failed-uploads.json', + JSON.stringify( { + 'ckeditor5-non-existing-01': [ 'Object not found. It may have been deleted or not been created yet.' ] + }, null, 2 ) + '\n', + 'utf-8' + ); - const storedErrors = JSON.parse( stubs.fs.writeFile.firstCall.args[ 1 ] ); - expect( storedErrors ).to.deep.equal( { - 'ckeditor5-non-existing-01': [ 'Object not found. It may have been deleted or not been created yet.' ] - } ); - } ); + expect( firstSpinner.finish ).toHaveBeenCalledTimes( 1 ); + expect( firstSpinner.finish ).toHaveBeenCalledWith( { emoji: '❌' } ); } ); - it( 'should store an error in the ".transifex-failed-uploads.json" file (cannot get a status of upload)', () => { + it( 'should store an error in the ".transifex-failed-uploads.json" file (cannot get a status of upload)', async () => { const error = { message: 'JsonApiError: 409', errors: [ @@ -573,31 +538,29 @@ describe( 'dev-transifex/upload()', () => { ] }; - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-01' ).rejects( error ); + vi.mocked( transifexService.getResourceUploadDetails ).mockRejectedValueOnce( error ); - return upload( config ) - .then( () => { - expect( stubs.logger.warning.callCount ).to.equal( 5 ); - expect( stubs.logger.warning.getCall( 2 ).args[ 0 ] ).to.equal( - 'Not all translations were uploaded due to errors in Transifex API.' - ); - expect( stubs.logger.warning.getCall( 3 ).args[ 0 ] ).to.equal( - 'Review the "/home/ckeditor5-with-errors/.transifex-failed-uploads.json" file for more details.' - ); - expect( stubs.logger.warning.getCall( 4 ).args[ 0 ] ).to.equal( - 'Re-running the script will process only packages specified in the file.' - ); + await upload( config ); - expect( stubs.fs.writeFile.callCount ).to.equal( 1 ); - expect( stubs.fs.writeFile.firstCall.args[ 0 ] ).to.equal( - '/home/ckeditor5-with-errors/.transifex-failed-uploads.json' - ); + expect( loggerWarningMock ).toHaveBeenCalledTimes( 5 ); + expect( loggerWarningMock ).toHaveBeenNthCalledWith( + 3, 'Not all translations were uploaded due to errors in Transifex API.' + ); + expect( loggerWarningMock ).toHaveBeenNthCalledWith( + 4, 'Review the "/home/ckeditor5-with-errors/.transifex-failed-uploads.json" file for more details.' + ); + expect( loggerWarningMock ).toHaveBeenNthCalledWith( + 5, 'Re-running the script will process only packages specified in the file.' + ); - const storedErrors = JSON.parse( stubs.fs.writeFile.firstCall.args[ 1 ] ); - expect( storedErrors ).to.deep.equal( { - 'ckeditor5-non-existing-01': [ 'Object not found. It may have been deleted or not been created yet.' ] - } ); - } ); + expect( vi.mocked( fs.writeFile ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs.writeFile ) ).toHaveBeenCalledWith( + '/home/ckeditor5-with-errors/.transifex-failed-uploads.json', + JSON.stringify( { + 'ckeditor5-non-existing-01': [ 'Object not found. It may have been deleted or not been created yet.' ] + }, null, 2 ) + '\n', + 'utf-8' + ); } ); } ); @@ -623,23 +586,12 @@ describe( 'dev-transifex/upload()', () => { projectName: 'ckeditor5' }; - for ( const [ packageName, packagePath ] of packages ) { - // Mock translation files. - stubs.fs.readFile.withArgs( config.cwd + '/' + packagePath + '/en.pot' ).resolves( '# ' + packageName ); - - // Mock Tx response when uploading a new translation content. - const uuid = 'uuid-' + packageName.match( /(\d+)$/ )[ 1 ]; - const withArgs = { - organizationName: 'ckeditor', - projectName: 'ckeditor5', - resourceName: packageName, - content: '# ' + packageName - }; - stubs.transifexService.createSourceFile.withArgs( withArgs ).resolves( uuid ); - } + vi.mocked( transifexService.createSourceFile ).mockResolvedValue( 'uuid-xx' ); + + vi.mocked( transifexService.createSourceFile ).mockResolvedValue( 'uuid-xx' ); // Mock resources on Transifex. - stubs.transifexService.getProjectData.resolves( { + vi.mocked( transifexService.getProjectData ).mockResolvedValue( { resources: [ { attributes: { name: 'ckeditor5-existing-11' } }, { attributes: { name: 'ckeditor5-existing-12' } }, @@ -648,77 +600,58 @@ describe( 'dev-transifex/upload()', () => { ] } ); - stubs.transifexService.createResource.resolves(); + vi.mocked( transifexService.createResource ).mockResolvedValue(); - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-01' ).resolves( - createResourceUploadDetailsResponse( 'ckeditor5-non-existing-01', 3, 0, 0 ) - ); - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-02' ).resolves( - createResourceUploadDetailsResponse( 'ckeditor5-non-existing-02', 0, 0, 0 ) - ); - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-03' ).resolves( - createResourceUploadDetailsResponse( 'ckeditor5-non-existing-03', 1, 0, 0 ) - ); - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-11' ).resolves( - createResourceUploadDetailsResponse( 'ckeditor5-existing-11', 0, 0, 0 ) - ); - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-12' ).resolves( - createResourceUploadDetailsResponse( 'ckeditor5-existing-12', 0, 1, 1 ) - ); - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-13' ).resolves( - createResourceUploadDetailsResponse( 'ckeditor5-existing-13', 2, 0, 0 ) - ); - stubs.transifexService.getResourceUploadDetails.withArgs( 'uuid-14' ).resolves( - createResourceUploadDetailsResponse( 'ckeditor5-existing-14', 0, 0, 0 ) - ); + vi.mocked( transifexService.getResourceUploadDetails ) + .mockResolvedValueOnce( createResourceUploadDetailsResponse( 'ckeditor5-existing-11', 0, 0, 0 ) ) + .mockResolvedValueOnce( createResourceUploadDetailsResponse( 'ckeditor5-existing-14', 0, 0, 0 ) ) + .mockResolvedValueOnce( createResourceUploadDetailsResponse( 'ckeditor5-non-existing-03', 1, 0, 0 ) ) + .mockResolvedValueOnce( createResourceUploadDetailsResponse( 'ckeditor5-non-existing-01', 3, 0, 0 ) ) + .mockResolvedValueOnce( createResourceUploadDetailsResponse( 'ckeditor5-existing-13', 2, 0, 0 ) ) + .mockResolvedValueOnce( createResourceUploadDetailsResponse( 'ckeditor5-non-existing-02', 0, 0, 0 ) ) + .mockResolvedValueOnce( createResourceUploadDetailsResponse( 'ckeditor5-existing-12', 0, 1, 1 ) ); - stubs.tools.createSpinner.returns( { - start: sinon.stub(), - finish: sinon.stub() + vi.mocked( tools.createSpinner ).mockReturnValue( { + start: vi.fn(), + finish: vi.fn() } ); } ); - it( 'should handle all packages', () => { - return upload( config ) - .then( () => { - expect( stubs.transifexService.getProjectData.callCount ).to.equal( 1 ); - expect( stubs.transifexService.createResource.callCount ).to.equal( 3 ); - expect( stubs.transifexService.createSourceFile.callCount ).to.equal( 7 ); - expect( stubs.transifexService.getResourceUploadDetails.callCount ).to.equal( 7 ); - expect( stubs.tools.createSpinner.callCount ).to.equal( 8 ); - } ); + it( 'should handle all packages', async () => { + await upload( config ); + + expect( vi.mocked( transifexService.getProjectData ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( transifexService.createResource ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( transifexService.createSourceFile ) ).toHaveBeenCalledTimes( 7 ); + expect( vi.mocked( transifexService.getResourceUploadDetails ) ).toHaveBeenCalledTimes( 7 ); + expect( vi.mocked( tools.createSpinner ) ).toHaveBeenCalledTimes( 8 ); } ); - it( 'should display a summary table with sorted packages (new, has changes, A-Z)', () => { - return upload( config ) - .then( () => { - expect( stubs.table.push.callCount ).to.equal( 1 ); - expect( stubs.table.push.firstCall.args ).to.be.an( 'array' ); - expect( stubs.table.push.firstCall.args ).to.lengthOf( 7 ); - - // 1x for printing "It takes a while", - // 5x for each column, x2 for each resource. - expect( stubs.chalk.gray.callCount ).to.equal( 11 ); - - expect( stubs.table.push.firstCall.args ).to.deep.equal( [ - [ 'ckeditor5-non-existing-01', '🆕', '3', '0', '0' ], - [ 'ckeditor5-non-existing-03', '🆕', '1', '0', '0' ], - [ 'ckeditor5-non-existing-02', '🆕', '0', '0', '0' ], - [ 'ckeditor5-existing-12', '', '0', '1', '1' ], - [ 'ckeditor5-existing-13', '', '2', '0', '0' ], - [ 'ckeditor5-existing-11', '', '0', '0', '0' ], - [ 'ckeditor5-existing-14', '', '0', '0', '0' ] - ] ); - } ); + it( 'should display a summary table with sorted packages (new, has changes, A-Z)', async () => { + await upload( config ); + + expect( vi.mocked( tablePushMock ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( tablePushMock ) ).toHaveBeenCalledWith( + [ 'ckeditor5-non-existing-01', '🆕', '3', '0', '0' ], + [ 'ckeditor5-non-existing-03', '🆕', '1', '0', '0' ], + [ 'ckeditor5-non-existing-02', '🆕', '0', '0', '0' ], + [ 'ckeditor5-existing-12', '', '0', '1', '1' ], + [ 'ckeditor5-existing-13', '', '2', '0', '0' ], + [ 'ckeditor5-existing-11', '', '0', '0', '0' ], + [ 'ckeditor5-existing-14', '', '0', '0', '0' ] + ); + + // 1x for printing "It takes a while", + // 5x for each column, x2 for each resource. + expect( vi.mocked( chalk.gray ) ).toHaveBeenCalledTimes( 11 ); } ); - it( 'should not display a summary table if none of the packages were processed', () => { + it( 'should not display a summary table if none of the packages were processed', async () => { config.packages = new Map(); - return upload( config ) - .then( () => { - expect( stubs.table.push.callCount ).to.equal( 0 ); - } ); + await upload( config ); + + expect( vi.mocked( tablePushMock ) ).toHaveBeenCalledTimes( 0 ); } ); } ); } ); @@ -726,11 +659,11 @@ describe( 'dev-transifex/upload()', () => { /** * Returns an object that looks like a response from Transifex API. * - * @param {String} packageName - * @param {Number} created - * @param {Number} updated - * @param {Number} deleted - * @returns {Object} + * @param {string} packageName + * @param {number} created + * @param {number} updated + * @param {number} deleted + * @returns {object} */ function createResourceUploadDetailsResponse( packageName, created, updated, deleted ) { return { diff --git a/packages/ckeditor5-dev-transifex/tests/utils.js b/packages/ckeditor5-dev-transifex/tests/utils.js index e36943da7..582b02a72 100644 --- a/packages/ckeditor5-dev-transifex/tests/utils.js +++ b/packages/ckeditor5-dev-transifex/tests/utils.js @@ -3,131 +3,120 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { verifyProperties, createLogger } from '../lib/utils.js'; -const sinon = require( 'sinon' ); -const chai = require( 'chai' ); -const expect = chai.expect; -const mockery = require( 'mockery' ); +import { logger } from '@ckeditor/ckeditor5-dev-utils'; +import chalk from 'chalk'; -describe( 'dev-transifex/utils', () => { - let stubs, utils; - - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - stubs = { - chalk: { - cyan: sinon.stub().callsFake( msg => msg ) - }, - logger: sinon.stub().returns( { - info: sinon.stub(), - warning: sinon.stub(), - error: sinon.stub(), - _log: sinon.stub() - } ) - }; +vi.mock( '@ckeditor/ckeditor5-dev-utils' ); +vi.mock( 'chalk', () => ( { + default: { + cyan: vi.fn( string => string ) + } +} ) ); - mockery.registerMock( 'chalk', stubs.chalk ); - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { - logger: stubs.logger +describe( 'dev-transifex/utils', () => { + let loggerInfoMock, loggerWarningMock, loggerErrorMock, loggerLogMock; + + beforeEach( async () => { + loggerInfoMock = vi.fn(); + loggerWarningMock = vi.fn(); + loggerErrorMock = vi.fn(); + loggerLogMock = vi.fn(); + + vi.mocked( logger ).mockImplementation( () => { + return { + info: loggerInfoMock, + warning: loggerWarningMock, + error: loggerErrorMock, + _log: loggerLogMock + }; } ); - - utils = require( '../lib/utils' ); - } ); - - afterEach( () => { - sinon.restore(); - mockery.deregisterAll(); - mockery.disable(); } ); describe( 'verifyProperties()', () => { it( 'should throw an error if the specified property is not specified in an object', () => { expect( () => { - utils.verifyProperties( {}, [ 'foo' ] ); + verifyProperties( {}, [ 'foo' ] ); } ).to.throw( Error, 'The specified object misses the following properties: foo.' ); } ); it( 'should throw an error if the value of the property is `undefined`', () => { expect( () => { - utils.verifyProperties( { foo: undefined }, [ 'foo' ] ); + verifyProperties( { foo: undefined }, [ 'foo' ] ); } ).to.throw( Error, 'The specified object misses the following properties: foo.' ); } ); it( 'should throw an error containing all The specified object misses the following properties', () => { expect( () => { - utils.verifyProperties( { foo: true, bar: 0 }, [ 'foo', 'bar', 'baz', 'xxx' ] ); + verifyProperties( { foo: true, bar: 0 }, [ 'foo', 'bar', 'baz', 'xxx' ] ); } ).to.throw( Error, 'The specified object misses the following properties: baz, xxx.' ); } ); it( 'should not throw an error if the value of the property is `null`', () => { expect( () => { - utils.verifyProperties( { foo: null }, [ 'foo' ] ); + verifyProperties( { foo: null }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is a boolean (`false`)', () => { expect( () => { - utils.verifyProperties( { foo: false }, [ 'foo' ] ); + verifyProperties( { foo: false }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is a boolean (`true`)', () => { expect( () => { - utils.verifyProperties( { foo: true }, [ 'foo' ] ); + verifyProperties( { foo: true }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is a number', () => { expect( () => { - utils.verifyProperties( { foo: 1 }, [ 'foo' ] ); + verifyProperties( { foo: 1 }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is a number (falsy value)', () => { expect( () => { - utils.verifyProperties( { foo: 0 }, [ 'foo' ] ); + verifyProperties( { foo: 0 }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is a NaN', () => { expect( () => { - utils.verifyProperties( { foo: NaN }, [ 'foo' ] ); + verifyProperties( { foo: NaN }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is a non-empty string', () => { expect( () => { - utils.verifyProperties( { foo: 'foo' }, [ 'foo' ] ); + verifyProperties( { foo: 'foo' }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is an empty string', () => { expect( () => { - utils.verifyProperties( { foo: '' }, [ 'foo' ] ); + verifyProperties( { foo: '' }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is an array', () => { expect( () => { - utils.verifyProperties( { foo: [] }, [ 'foo' ] ); + verifyProperties( { foo: [] }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is an object', () => { expect( () => { - utils.verifyProperties( { foo: {} }, [ 'foo' ] ); + verifyProperties( { foo: {} }, [ 'foo' ] ); } ).to.not.throw( Error ); } ); it( 'should not throw an error if the value of the property is a function', () => { expect( () => { - utils.verifyProperties( { + verifyProperties( { foo: () => {} }, [ 'foo' ] ); } ).to.not.throw( Error ); @@ -136,39 +125,39 @@ describe( 'dev-transifex/utils', () => { describe( 'createLogger()', () => { it( 'should be a function', () => { - expect( utils.createLogger ).to.be.a( 'function' ); + expect( createLogger ).toBeInstanceOf( Function ); } ); it( 'should return an object with methods', () => { - const logger = utils.createLogger(); + const logger = createLogger(); - expect( logger ).to.be.an( 'object' ); - expect( logger.progress ).to.be.a( 'function' ); - expect( logger.info ).to.be.a( 'function' ); - expect( logger.warning ).to.be.a( 'function' ); - expect( logger.error ).to.be.a( 'function' ); - expect( logger._log ).to.be.a( 'function' ); + expect( logger ).toBeInstanceOf( Object ); + expect( logger.progress ).toBeInstanceOf( Function ); + expect( logger.info ).toBeInstanceOf( Function ); + expect( logger.warning ).toBeInstanceOf( Function ); + expect( logger.error ).toBeInstanceOf( Function ); + expect( logger._log ).toBeInstanceOf( Function ); } ); it( 'should call the info method for a non-empty progress message', () => { - const logger = utils.createLogger(); + const logger = createLogger(); logger.progress( 'Example step.' ); - expect( logger.info.callCount ).to.equal( 1 ); - expect( logger.info.firstCall.args[ 0 ] ).to.equal( '\n📍 Example step.' ); - expect( stubs.chalk.cyan.callCount ).to.equal( 1 ); - expect( stubs.chalk.cyan.firstCall.args[ 0 ] ).to.equal( 'Example step.' ); + expect( loggerInfoMock ).toHaveBeenCalledTimes( 1 ); + expect( loggerInfoMock ).toHaveBeenCalledWith( '\n📍 Example step.' ); + expect( chalk.cyan ).toHaveBeenCalledTimes( 1 ); + expect( chalk.cyan ).toHaveBeenCalledWith( 'Example step.' ); } ); it( 'should call the info method with an empty message for an empty progress message', () => { - const logger = utils.createLogger(); + const logger = createLogger(); logger.progress(); - expect( logger.info.callCount ).to.equal( 1 ); - expect( logger.info.firstCall.args[ 0 ] ).to.equal( '' ); - expect( stubs.chalk.cyan.called ).to.equal( false ); + expect( loggerInfoMock ).toHaveBeenCalledTimes( 1 ); + expect( loggerInfoMock ).toHaveBeenCalledWith( '' ); + expect( chalk.cyan ).toHaveBeenCalledTimes( 0 ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-transifex/vitest.config.js b/packages/ckeditor5-dev-transifex/vitest.config.js new file mode 100644 index 000000000..5ad784a28 --- /dev/null +++ b/packages/ckeditor5-dev-transifex/vitest.config.js @@ -0,0 +1,23 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig( { + test: { + testTimeout: 10000, + restoreMocks: true, + include: [ + 'tests/**/*.js' + ], + coverage: { + provider: 'v8', + include: [ + 'lib/**' + ], + reporter: [ 'text', 'json', 'html', 'lcov' ] + } + } +} ); diff --git a/packages/ckeditor5-dev-translations/lib/ckeditortranslationsplugin.js b/packages/ckeditor5-dev-translations/lib/ckeditortranslationsplugin.js index 5c9d01471..d789479b8 100644 --- a/packages/ckeditor5-dev-translations/lib/ckeditortranslationsplugin.js +++ b/packages/ckeditor5-dev-translations/lib/ckeditortranslationsplugin.js @@ -3,11 +3,9 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const chalk = require( 'chalk' ); -const serveTranslations = require( './servetranslations' ); -const MultipleLanguageTranslationService = require( './multiplelanguagetranslationservice' ); +import chalk from 'chalk'; +import serveTranslations from './servetranslations.js'; +import MultipleLanguageTranslationService from './multiplelanguagetranslationservice.js'; /** * CKEditorTranslationsPlugin, for now, consists only of the translation mechanism (@ckeditor/ckeditor5#624, @ckeditor/ckeditor5#387, @@ -29,7 +27,7 @@ const MultipleLanguageTranslationService = require( './multiplelanguagetranslati * This plugin tries to clean the output translation directory before each build to make sure, that all translations are correct. * See https://github.com/ckeditor/ckeditor5/issues/700 for more information. */ -module.exports = class CKEditorTranslationsPlugin { +export default class CKEditorTranslationsPlugin { /** * @param {CKEditorTranslationsPluginOptions} options Plugin options. */ @@ -102,41 +100,41 @@ module.exports = class CKEditorTranslationsPlugin { serveTranslations( compiler, this.options, translationService ); } -}; +} /** * @callback AssetNamesFilter - * @param {String} name Webpack asset name/path - * @returns {Boolean} + * @param {string} name Webpack asset name/path + * @returns {boolean} */ /** - * @typedef {Object} CKEditorTranslationsPluginOptions CKEditorTranslationsPluginOptions options. + * @typedef {object} CKEditorTranslationsPluginOptions CKEditorTranslationsPluginOptions options. * - * @property {String} language The main language for internationalization - translations for that language + * @property {string} language The main language for internationalization - translations for that language * will be added to the bundle(s). - * @property {Array.|'all'} [additionalLanguages] Additional languages. Build is optimized when this option is not set. + * @property {Array.|'all'} [additionalLanguages] Additional languages. Build is optimized when this option is not set. * When `additionalLanguages` is set to 'all' then script will be looking for all languages and according translations during * the compilation. - * @property {String} [outputDirectory='translations'] The output directory for the emitted translation files, + * @property {string} [outputDirectory='translations'] The output directory for the emitted translation files, * should be relative to the webpack context. - * @property {Boolean} [strict] An option that make the plugin throw when the error is found during the compilation. - * @property {Boolean} [verbose] An option that make this plugin log all warnings into the console. - * @property {Boolean} [addMainLanguageTranslationsToAllAssets] An option that allows outputting translations to more than one + * @property {boolean} [strict] An option that make the plugin throw when the error is found during the compilation. + * @property {boolean} [verbose] An option that make this plugin log all warnings into the console. + * @property {boolean} [addMainLanguageTranslationsToAllAssets] An option that allows outputting translations to more than one * JS asset. - * @property {String} [corePackageSampleResourcePath] A path (ES6 import) to the file that determines whether the `ckeditor5-core` package + * @property {string} [corePackageSampleResourcePath] A path (ES6 import) to the file that determines whether the `ckeditor5-core` package * exists. The package contains common translations used by many packages. To avoid duplications, they are shared by the core package. - * @property {String} [corePackageContextsResourcePath] A path (ES6 import) to the file where all contexts are specified + * @property {string} [corePackageContextsResourcePath] A path (ES6 import) to the file where all contexts are specified * for the `ckeditor5-core` package. - * @property {Boolean} [buildAllTranslationsToSeparateFiles] An option that makes all translations output to separate files. - * @property {String} [sourceFilesPattern] An option that allows override the default pattern for CKEditor 5 source files. - * @property {String} [packageNamesPattern] An option that allows override the default pattern for CKEditor 5 package names. - * @property {String} [corePackagePattern] An option that allows override the default CKEditor 5 core package pattern. - * @property {String|Function|RegExp} [translationsOutputFile] An option allowing outputting all translation file to the given file. + * @property {boolean} [buildAllTranslationsToSeparateFiles] An option that makes all translations output to separate files. + * @property {string} [sourceFilesPattern] An option that allows override the default pattern for CKEditor 5 source files. + * @property {string} [packageNamesPattern] An option that allows override the default pattern for CKEditor 5 package names. + * @property {string} [corePackagePattern] An option that allows override the default CKEditor 5 core package pattern. + * @property {string|Function|RegExp} [translationsOutputFile] An option allowing outputting all translation file to the given file. * If a file specified by a path (string) does not exist, then it will be created. Otherwise, translations will be outputted to the file. - * @property {Boolean} [includeCorePackageTranslations=false] A flag that determines whether all translations found in the core package + * @property {boolean} [includeCorePackageTranslations=false] A flag that determines whether all translations found in the core package * should be added to the output bundle file. If set to true, translations from the core package will be saved even if are not * used in the source code (*.js files). - * @property {Boolean} [skipPluralFormFunction=false] Whether the `getPluralForm()` function should be added in the output bundle file. + * @property {boolean} [skipPluralFormFunction=false] Whether the `getPluralForm()` function should be added in the output bundle file. * @property {AssetNamesFilter} [assetNamesFilter] A function to filter assets probably importing CKEditor 5 modules. */ diff --git a/packages/ckeditor5-dev-translations/lib/cleanpofilecontent.js b/packages/ckeditor5-dev-translations/lib/cleanpofilecontent.js index 3d2761c4f..e695ab4e6 100644 --- a/packages/ckeditor5-dev-translations/lib/cleanpofilecontent.js +++ b/packages/ckeditor5-dev-translations/lib/cleanpofilecontent.js @@ -3,20 +3,18 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const PO = require( 'pofile' ); +import PO from 'pofile'; /** * Returns translations stripped from the personal data, but with an added banner * containing information where to add new translations or fix the existing ones. * - * @param {String} poFileContent Content of the translation file. - * @param {Object} [options={}] - * @param {Boolean} [options.simplifyLicenseHeader] Whether to skip adding the contribute URL in the header. - * @returns {String} + * @param {string} poFileContent Content of the translation file. + * @param {object} [options={}] + * @param {boolean} [options.simplifyLicenseHeader] Whether to skip adding the contribute URL in the header. + * @returns {string} */ -module.exports = function cleanPoFileContent( poFileContent, options = {} ) { +export default function cleanPoFileContent( poFileContent, options = {} ) { const po = PO.parse( poFileContent ); // Remove personal data from headers. @@ -53,4 +51,4 @@ module.exports = function cleanPoFileContent( poFileContent, options = {} ) { } return po.toString(); -}; +} diff --git a/packages/ckeditor5-dev-translations/lib/createdictionaryfrompofilecontent.js b/packages/ckeditor5-dev-translations/lib/createdictionaryfrompofilecontent.js index c828e2095..9cf6a83a3 100644 --- a/packages/ckeditor5-dev-translations/lib/createdictionaryfrompofilecontent.js +++ b/packages/ckeditor5-dev-translations/lib/createdictionaryfrompofilecontent.js @@ -3,17 +3,15 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const PO = require( 'pofile' ); +import PO from 'pofile'; /** * Returns object with key-value pairs from parsed po file. * - * @param {String} poFileContent Content of the translation file. + * @param {string} poFileContent Content of the translation file. * @returns {Object.} */ -module.exports = function createDictionaryFromPoFileContent( poFileContent ) { +export default function createDictionaryFromPoFileContent( poFileContent ) { const po = PO.parse( poFileContent ); const keys = {}; @@ -26,4 +24,4 @@ module.exports = function createDictionaryFromPoFileContent( poFileContent ) { } return keys; -}; +} diff --git a/packages/ckeditor5-dev-translations/lib/findmessages.js b/packages/ckeditor5-dev-translations/lib/findmessages.js index a926427dc..a228d70a1 100644 --- a/packages/ckeditor5-dev-translations/lib/findmessages.js +++ b/packages/ckeditor5-dev-translations/lib/findmessages.js @@ -3,21 +3,19 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const parser = require( '@babel/parser' ); -const traverse = require( '@babel/traverse' ).default; +import parser from '@babel/parser'; +import { default as traverse } from '@babel/traverse'; /** * Parses source and finds messages from the first argument of `t()` calls. * - * @param {String} source A content of the JS file that will be translated. - * @param {String} sourceFile A path to source file, used only for creating error messages. + * @param {string} source A content of the JS file that will be translated. + * @param {string} sourceFile A path to source file, used only for creating error messages. * @param {(msg: Message) => void} onMessageFound * @param {(err: string) => void} onErrorFound - * @returns {String} Transformed source. + * @returns {string} Transformed source. */ -module.exports = function findMessages( source, sourceFile, onMessageFound, onErrorFound ) { +export default function findMessages( source, sourceFile, onMessageFound, onErrorFound ) { const ast = parser.parse( source, { sourceType: 'module', ranges: true, @@ -26,7 +24,10 @@ module.exports = function findMessages( source, sourceFile, onMessageFound, onEr ] } ); - traverse( ast, { + // Support for a non-`type=module` project. + const traverseCallable = typeof traverse === 'function' ? traverse : traverse.default; + + traverseCallable( ast, { CallExpression: ( { node } ) => { try { findMessagesInNode( node ); @@ -111,7 +112,7 @@ module.exports = function findMessages( source, sourceFile, onMessageFound, onEr `First t() call argument should be a string literal or an object literal (${ sourceFile }).` ); } -}; +} // Get property from the list of properties // It supports both forms: `{ propertyName: foo }` and `{ 'propertyName': 'foo' }` @@ -143,9 +144,9 @@ function isTMethodCallExpression( node ) { } /** - * @typedef {Object} Message + * @typedef {object} Message * - * @property {String} id - * @property {String} string - * @property {String} [plural] + * @property {string} id + * @property {string} string + * @property {string} [plural] */ diff --git a/packages/ckeditor5-dev-translations/lib/index.js b/packages/ckeditor5-dev-translations/lib/index.js index b42e10c11..db5991323 100644 --- a/packages/ckeditor5-dev-translations/lib/index.js +++ b/packages/ckeditor5-dev-translations/lib/index.js @@ -3,18 +3,8 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const findMessages = require( './findmessages' ); -const cleanPoFileContent = require( './cleanpofilecontent' ); -const MultipleLanguageTranslationService = require( './multiplelanguagetranslationservice' ); -const createDictionaryFromPoFileContent = require( './createdictionaryfrompofilecontent' ); -const CKEditorTranslationsPlugin = require( './ckeditortranslationsplugin' ); - -module.exports = { - findMessages, - cleanPoFileContent, - MultipleLanguageTranslationService, - createDictionaryFromPoFileContent, - CKEditorTranslationsPlugin -}; +export { default as findMessages } from './findmessages.js'; +export { default as cleanPoFileContent } from './cleanpofilecontent.js'; +export { default as MultipleLanguageTranslationService } from './multiplelanguagetranslationservice.js'; +export { default as createDictionaryFromPoFileContent } from './createdictionaryfrompofilecontent.js'; +export { default as CKEditorTranslationsPlugin } from './ckeditortranslationsplugin.js'; diff --git a/packages/ckeditor5-dev-translations/lib/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-translations/lib/multiplelanguagetranslationservice.js index b800b7074..2064768b2 100644 --- a/packages/ckeditor5-dev-translations/lib/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-translations/lib/multiplelanguagetranslationservice.js @@ -3,29 +3,28 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const fs = require( 'fs' ); -const findMessages = require( './findmessages' ); -const { EventEmitter } = require( 'events' ); -const PO = require( 'pofile' ); +import path from 'path'; +import fs from 'fs'; +import findMessages from './findmessages.js'; +import { EventEmitter } from 'events'; +import PO from 'pofile'; /** * A service that serves translations assets based on the found PO files in the registered packages. */ -module.exports = class MultipleLanguageTranslationService extends EventEmitter { +export default class MultipleLanguageTranslationService extends EventEmitter { + // TODO maybe fix the jsdoc types /** - * @param {Object} options - * @param {String} options.mainLanguage The target language that will be bundled into the main webpack asset. - * @param {Array.} [options.additionalLanguages] Additional languages which files will be emitted. + * @param {object} options + * @param {string} options.mainLanguage The target language that will be bundled into the main webpack asset. + * @param {Array.} [options.additionalLanguages] Additional languages which files will be emitted. * When option is set to 'all', all languages found during the compilation will be added. - * @param {Boolean} [options.compileAllLanguages] When set to `true` languages will be found at runtime. - * @param {Boolean} [options.addMainLanguageTranslationsToAllAssets] When set to `true` the service will not complain + * @param {boolean} [options.compileAllLanguages] When set to `true` languages will be found at runtime. + * @param {boolean} [options.addMainLanguageTranslationsToAllAssets] When set to `true` the service will not complain * about multiple JS assets and will output translations for the main language to all found assets. - * @param {Boolean} [options.buildAllTranslationsToSeparateFiles] When set to `true` the service will output all translations + * @param {boolean} [options.buildAllTranslationsToSeparateFiles] When set to `true` the service will output all translations * to separate files. - * @param {String|Function|RegExp} [options.translationsOutputFile] An option allowing outputting all translation file + * @param {string|Function|RegExp} [options.translationsOutputFile] An option allowing outputting all translation file * to the given file. If a file specified by a path (string) does not exist, then it will be created. Otherwise, translations * will be outputted to the file. */ @@ -44,7 +43,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Main language that should be built in to the bundle. * * @private - * @type {String} + * @type {string} */ this._mainLanguage = mainLanguage; @@ -53,7 +52,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * if the `compileAllLanguages` flag is turned on. * * @private - * @type {Set.} + * @type {Set.} */ this._languages = new Set( [ mainLanguage, ...additionalLanguages ] ); @@ -61,7 +60,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * An option indicating if the languages should be found at runtime. * * @private - * @type {Boolean} + * @type {boolean} */ this._compileAllLanguages = compileAllLanguages; @@ -70,7 +69,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * and will add translation for the main language to all of them. Useful option for manual tests, etc. * * @private - * @type {Boolean} + * @type {boolean} */ this._addMainLanguageTranslationsToAllAssets = addMainLanguageTranslationsToAllAssets; @@ -78,7 +77,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * A boolean option. When set to `true` outputs all translations to separate files. * * @private - * @type {Boolean} + * @type {boolean} */ this._buildAllTranslationsToSeparateFiles = buildAllTranslationsToSeparateFiles; @@ -86,7 +85,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * A set of handled packages that speeds up the translation process. * * @private - * @type {Set.} + * @type {Set.} */ this._handledPackages = new Set(); @@ -94,7 +93,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * A map of translation dictionaries in the `language -> messageId -> single & plural forms` format. * * @private - * @type {Object.>>} + * @type {Object.>>} */ this._translationDictionaries = {}; @@ -102,7 +101,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Plural form rules that will be added to generated translation assets. * * @private - * @type {Object.} + * @type {Object.} */ this._pluralFormsRules = {}; @@ -111,7 +110,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * (with a single and possible plural forms) should be found for the target languages. * * @private - * @type {Set.} + * @type {Set.} */ this._foundMessageIds = new Set(); @@ -119,7 +118,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Whether the `getPluralForm` function should be added in the bundle file. * * @private - * @type {Boolean} + * @type {boolean} */ this._skipPluralFormFunction = skipPluralFormFunction; @@ -131,9 +130,9 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * (e.g. an incorrect `t()` call). * * @fires warning - * @param {String} source Content of the source file. - * @param {String} fileName Source file name - * @returns {String} + * @param {string} source Content of the source file. + * @param {string} fileName Source file name + * @returns {string} */ translateSource( source, fileName ) { findMessages( @@ -151,7 +150,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * If the `compileAllLanguages` flag is set to `true`, then the language set will be expanded to all found languages. * * @fires warning - * @param {String} pathToPackage A path to the package containing translations. + * @param {string} pathToPackage A path to the package containing translations. */ loadPackage( pathToPackage ) { if ( this._handledPackages.has( pathToPackage ) ) { @@ -199,10 +198,10 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * * @fires warning * @fires error - * @param {Object} options - * @param {String} options.outputDirectory Output directory for the translation files relative to the output. - * @param {Array.} options.compilationAssetNames Original asset names from the compiler (e.g. Webpack). - * @returns {Array.} Returns new and modified assets that will be added to original ones. + * @param {object} options + * @param {string} options.outputDirectory Output directory for the translation files relative to the output. + * @param {Array.} options.compilationAssetNames Original asset names from the compiler (e.g. Webpack). + * @returns {Array.} Returns new and modified assets that will be added to original ones. */ getAssets( { outputDirectory, compilationAssetNames } ) { let bundledLanguage = this._mainLanguage; @@ -251,17 +250,17 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { /** * Adds the specified `id` to the collection which will be translated to the specified language. * - * @param {String} id + * @param {string} id */ addIdMessage( id ) { this._foundMessageIds.add( id ); } /** - * @param {Object} options - * @param {String} options.outputDirectory Output directory for the translation files relative to the output. - * @param {Array.} options.compilationAssetNames Original asset names from the compiler (e.g. Webpack). - * @returns {Array.} Returns an array with one asset that + * @param {object} options + * @param {string} options.outputDirectory Output directory for the translation files relative to the output. + * @param {Array.} options.compilationAssetNames Original asset names from the compiler (e.g. Webpack). + * @returns {Array.} Returns an array with one asset that */ _getAssetsWithTranslationsBundledToTheOutputFile( { outputDirectory, compilationAssetNames } ) { const assetName = match( this._translationsOutputFile, compilationAssetNames ); @@ -286,8 +285,8 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Returns assets for the given directory and languages. * * @private - * @param {String} outputDirectory The output directory for assets. - * @param {Array.} languages Languages for assets. + * @param {string} outputDirectory The output directory for assets. + * @param {Array.} languages Languages for assets. */ _getTranslationAssets( outputDirectory, languages ) { // Sort the array of message ids to provide deterministic results. @@ -308,7 +307,7 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { // pluralForms="nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2)" // pluralForms="nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2" - /** @type {String} */ + /** @type {string} */ const pluralFormsRule = this._pluralFormsRules[ language ]; let pluralFormFunction; @@ -350,9 +349,9 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Skips messages that lacks their translations. * * @private - * @param {String} language The target language - * @param {Array.} sortedMessageIds An array of sorted message ids. - * @returns {Object.} + * @param {string} language The target language + * @param {Array.} sortedMessageIds An array of sorted message ids. + * @returns {Object.} */ _getTranslations( language, sortedMessageIds ) { const langDictionary = this._translationDictionaries[ language ]; @@ -380,8 +379,8 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Loads translations from the PO file if that file exists. * * @private - * @param {String} language PO file's language. - * @param {String} pathToPoFile Path to the target PO file. + * @param {string} language PO file's language. + * @param {string} pathToPoFile Path to the target PO file. */ _loadPoFile( language, pathToPoFile ) { if ( !fs.existsSync( pathToPoFile ) ) { @@ -407,8 +406,8 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { * Returns a path to the translation directory depending on the path to the package. * * @protected - * @param {String|null} relativePathToPackage - * @returns {String} + * @param {string|null} relativePathToPackage + * @returns {string} */ _getPathToTranslationDirectory( relativePathToPackage ) { // If the `relativePathToPackage` is not specified, translations for a single package are processed. @@ -418,12 +417,12 @@ module.exports = class MultipleLanguageTranslationService extends EventEmitter { return path.join( 'lang', 'translations' ); } -}; +} /** - * @param {String|Function|RegExp} predicate - * @param {Array.} options - * @returns {String|undefined} + * @param {string|Function|RegExp} predicate + * @param {Array.} options + * @returns {string|undefined} */ function match( predicate, options ) { if ( typeof predicate === 'function' ) { diff --git a/packages/ckeditor5-dev-translations/lib/servetranslations.js b/packages/ckeditor5-dev-translations/lib/servetranslations.js index 30ad1ba6c..d854a6d3f 100644 --- a/packages/ckeditor5-dev-translations/lib/servetranslations.js +++ b/packages/ckeditor5-dev-translations/lib/servetranslations.js @@ -3,13 +3,17 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import fs from 'fs-extra'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import chalk from 'chalk'; +import { rimraf } from 'rimraf'; +import webpackSources from 'webpack-sources'; -const chalk = require( 'chalk' ); -const rimraf = require( 'rimraf' ); -const fs = require( 'fs' ); -const path = require( 'path' ); -const { RawSource, ConcatSource } = require( 'webpack-sources' ); +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); + +const { RawSource, ConcatSource } = webpackSources; /** * Serve translations depending on the used translation service and passed options. @@ -17,19 +21,19 @@ const { RawSource, ConcatSource } = require( 'webpack-sources' ); * * See https://webpack.js.org/api/compiler/#event-hooks and https://webpack.js.org/api/compilation/ for details about specific hooks. * - * @param {Object} compiler The webpack compiler. - * @param {Object} options Translation options. - * @param {String} options.outputDirectory The output directory for the emitted translation files, relative to the webpack context. - * @param {Boolean} [options.strict] An option that make this function throw when the error is found during the compilation. - * @param {Boolean} [options.verbose] An option that make this function log everything into the console. - * @param {String} [options.sourceFilesPattern] The source files pattern - * @param {String} [options.packageNamesPattern] The package names pattern. - * @param {String} [options.corePackagePattern] The core package pattern. + * @param {object} compiler The webpack compiler. + * @param {object} options Translation options. + * @param {string} options.outputDirectory The output directory for the emitted translation files, relative to the webpack context. + * @param {boolean} [options.strict] An option that make this function throw when the error is found during the compilation. + * @param {boolean} [options.verbose] An option that make this function log everything into the console. + * @param {string} [options.sourceFilesPattern] The source files pattern + * @param {string} [options.packageNamesPattern] The package names pattern. + * @param {string} [options.corePackagePattern] The core package pattern. * @param {AssetNamesFilter} [options.assetNamesFilter] A function to filter assets probably importing CKEditor 5 modules. * @param {TranslationService} translationService Translation service that will load PO files, replace translation keys and generate assets. * ckeditor5 - independent without hard-to-test logic. */ -module.exports = function serveTranslations( compiler, options, translationService ) { +export default function serveTranslations( compiler, options, translationService ) { const cwd = process.cwd(); // A set of unique messages that prevents message duplications. @@ -84,7 +88,7 @@ module.exports = function serveTranslations( compiler, options, translationServi } // Add all context messages found in the core package. - const contexts = require( pathToResource ); + const contexts = fs.readJsonSync( pathToResource ); for ( const item of Object.keys( contexts ) ) { translationService.addIdMessage( item ); @@ -103,6 +107,7 @@ module.exports = function serveTranslations( compiler, options, translationServi // after any potential TypeScript file has already been compiled. module.loaders.unshift( { loader: path.join( __dirname, 'translatesourceloader.js' ), + type: 'module', options: { translateSource } } ); @@ -176,14 +181,14 @@ module.exports = function serveTranslations( compiler, options, translationServi console.warn( chalk.yellow( `[CKEditorTranslationsPlugin] Warning: ${ warning }` ) ); } } -}; +} /** * Return path to the package if the resource comes from `ckeditor5-*` package. * - * @param {String} cwd Current working directory. - * @param {String} resource Absolute path to the resource. - * @returns {String|null} + * @param {string} cwd Current working directory. + * @param {string} resource Absolute path to the resource. + * @returns {string|null} */ function getPathToPackage( cwd, resource, packageNamePattern ) { const relativePathToResource = path.relative( cwd, resource ); @@ -204,7 +209,7 @@ function getPathToPackage( cwd, resource, packageNamePattern ) { * * @param {webpack.Compiler} compiler * @param {webpack.Compilation} compilation - * @returns {Object} + * @returns {object} */ function getCompilationHooks( compiler, compilation ) { const { webpack } = compiler; @@ -222,8 +227,8 @@ function getCompilationHooks( compiler, compilation ) { /** * Returns an object with the chunk assets depending on the Webpack version. * - * @param {Object} compilation - * @returns {Object} + * @param {object} compilation + * @returns {object} */ function getChunkAssets( compilation ) { // Webpack 5 vs Webpack 4. @@ -233,7 +238,7 @@ function getChunkAssets( compilation ) { /** * Returns an array with list of loaded files depending on the Webpack version. * - * @param {Object|Array} chunks + * @param {object|Array} chunks * @returns {Array} */ function getFilesFromChunks( chunks ) { @@ -258,22 +263,22 @@ function getFilesFromChunks( chunks ) { * Load package translations. * * @method #loadPackage - * @param {String} pathToPackage Path to the package. + * @param {string} pathToPackage Path to the package. */ /** * Translate file's source to the target language. * * @method #translateSource - * @param {String} source File's source. - * @returns {String} + * @param {string} source File's source. + * @returns {string} */ /** * Get assets at the end of compilation. * * @method #getAssets - * @returns {Array.} + * @returns {Array.} */ /** diff --git a/packages/ckeditor5-dev-translations/lib/translatesourceloader.js b/packages/ckeditor5-dev-translations/lib/translatesourceloader.js index 56b47911a..bcad21b96 100644 --- a/packages/ckeditor5-dev-translations/lib/translatesourceloader.js +++ b/packages/ckeditor5-dev-translations/lib/translatesourceloader.js @@ -3,17 +3,15 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /** * Very simple loader that runs the translateSource function only on the source. * translateSource is provided by the CKEditorTranslationsPlugin. * - * @param {String} source Content of the resource file - * @param {Object} map A source map consumed by the `source-map` package. + * @param {string} source Content of the resource file + * @param {object} map A source map consumed by the `source-map` package. */ -module.exports = function translateSourceLoader( source, map ) { +export default function translateSourceLoader( source, map ) { const output = this.query.translateSource( source, this.resourcePath ); this.callback( null, output, map ); -}; +} diff --git a/packages/ckeditor5-dev-translations/package.json b/packages/ckeditor5-dev-translations/package.json index 8f7f9735f..27d2f641a 100644 --- a/packages/ckeditor5-dev-translations/package.json +++ b/packages/ckeditor5-dev-translations/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-translations", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "CKEditor 5 translations plugin for webpack.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -17,25 +17,24 @@ "npm": ">=5.7.1" }, "main": "lib/index.js", + "type": "module", "files": [ "lib" ], "dependencies": { "@babel/parser": "^7.18.9", "@babel/traverse": "^7.18.9", - "chalk": "^4.0.0", - "rimraf": "^3.0.2", - "webpack-sources": "^2.0.1", + "chalk": "^5.0.0", + "fs-extra": "^11.0.0", + "rimraf": "^5.0.0", + "webpack-sources": "^3.0.0", "pofile": "^1.0.9" }, "devDependencies": { - "chai": "^4.2.0", - "mocha": "^7.1.2", - "proxyquire": "^2.1.3", - "sinon": "^9.2.4" + "vitest": "^2.0.5" }, "scripts": { - "test": "mocha './tests/**/*.js' --timeout 10000", - "coverage": "nyc --reporter=lcov --reporter=text-summary yarn run test" + "test": "vitest run --config vitest.config.js", + "coverage": "vitest run --config vitest.config.js --coverage" } } diff --git a/packages/ckeditor5-dev-translations/tests/ckeditortranslationsplugin.js b/packages/ckeditor5-dev-translations/tests/ckeditortranslationsplugin.js index fab4df076..e4f14ef1e 100644 --- a/packages/ckeditor5-dev-translations/tests/ckeditortranslationsplugin.js +++ b/packages/ckeditor5-dev-translations/tests/ckeditortranslationsplugin.js @@ -3,35 +3,15 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi } from 'vitest'; +import CKEditorTranslationsPlugin from '../lib/ckeditortranslationsplugin.js'; +import serveTranslations from '../lib/servetranslations.js'; +import MultipleLanguageTranslationService from '../lib/multiplelanguagetranslationservice.js'; -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const proxyquire = require( 'proxyquire' ); +vi.mock( '../lib/servetranslations' ); +vi.mock( '../lib/multiplelanguagetranslationservice', () => ( { default: vi.fn() } ) ); describe( 'dev-translations/CKEditorTranslationsPlugin', () => { - let sandbox, CKEditorTranslationsPlugin, stubs; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - serveTranslations: sandbox.stub().returns( {} ), - MultipleLanguageTranslationService: sandbox.stub().returns( {} ) - }; - - CKEditorTranslationsPlugin = proxyquire( '../lib/ckeditortranslationsplugin', { - './servetranslations': stubs.serveTranslations, - './multiplelanguagetranslationservice': stubs.MultipleLanguageTranslationService - } ); - - sandbox.stub( console, 'warn' ); - } ); - - afterEach( () => { - sandbox.restore(); - } ); - describe( 'constructor()', () => { it( 'should initialize with passed options', () => { const options = { language: 'pl' }; @@ -128,7 +108,7 @@ describe( 'dev-translations/CKEditorTranslationsPlugin', () => { const ckEditorTranslationsPlugin = new CKEditorTranslationsPlugin( options ); ckEditorTranslationsPlugin.apply( compiler ); - sinon.assert.calledOnce( stubs.serveTranslations ); + expect( serveTranslations ).toHaveBeenCalledOnce(); } ); describe( 'should create an instance of `MultipleLanguageTranslationService`', () => { @@ -140,21 +120,18 @@ describe( 'dev-translations/CKEditorTranslationsPlugin', () => { const ckEditorTranslationsPlugin = new CKEditorTranslationsPlugin( options ); ckEditorTranslationsPlugin.apply( {} ); - sinon.assert.calledOnce( stubs.serveTranslations ); - - sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); - sinon.assert.calledWithExactly( - stubs.MultipleLanguageTranslationService, - { - mainLanguage: 'pl', - compileAllLanguages: false, - additionalLanguages: [], - buildAllTranslationsToSeparateFiles: false, - addMainLanguageTranslationsToAllAssets: false, - translationsOutputFile: undefined, - skipPluralFormFunction: false - } - ); + expect( serveTranslations ).toHaveBeenCalledOnce(); + + expect( MultipleLanguageTranslationService ).toHaveBeenCalledOnce(); + expect( MultipleLanguageTranslationService ).toHaveBeenCalledWith( expect.objectContaining( { + mainLanguage: 'pl', + compileAllLanguages: false, + additionalLanguages: [], + buildAllTranslationsToSeparateFiles: false, + addMainLanguageTranslationsToAllAssets: false, + translationsOutputFile: undefined, + skipPluralFormFunction: false + } ) ); } ); it( 'for additional languages provided', () => { @@ -166,24 +143,23 @@ describe( 'dev-translations/CKEditorTranslationsPlugin', () => { const ckEditorTranslationsPlugin = new CKEditorTranslationsPlugin( options ); ckEditorTranslationsPlugin.apply( {} ); - sinon.assert.calledOnce( stubs.serveTranslations ); - - sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); - sinon.assert.calledWithExactly( - stubs.MultipleLanguageTranslationService, - { - mainLanguage: 'pl', - compileAllLanguages: false, - additionalLanguages: [ 'en' ], - buildAllTranslationsToSeparateFiles: false, - addMainLanguageTranslationsToAllAssets: false, - translationsOutputFile: undefined, - skipPluralFormFunction: false - } - ); + expect( serveTranslations ).toHaveBeenCalledOnce(); + + expect( MultipleLanguageTranslationService ).toHaveBeenCalledOnce(); + expect( MultipleLanguageTranslationService ).toHaveBeenCalledWith( expect.objectContaining( { + mainLanguage: 'pl', + compileAllLanguages: false, + additionalLanguages: [ 'en' ], + buildAllTranslationsToSeparateFiles: false, + addMainLanguageTranslationsToAllAssets: false, + translationsOutputFile: undefined, + skipPluralFormFunction: false + } ) ); } ); it( 'for `additionalLanguages` set to `all`', () => { + const consoleWarnSpy = vi.spyOn( console, 'warn' ); + const options = { language: 'en', additionalLanguages: 'all' @@ -192,23 +168,20 @@ describe( 'dev-translations/CKEditorTranslationsPlugin', () => { const ckEditorTranslationsPlugin = new CKEditorTranslationsPlugin( options ); ckEditorTranslationsPlugin.apply( {} ); - sinon.assert.calledOnce( stubs.serveTranslations ); - - sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); - sinon.assert.calledWithExactly( - stubs.MultipleLanguageTranslationService, - { - mainLanguage: 'en', - compileAllLanguages: true, - additionalLanguages: [], - buildAllTranslationsToSeparateFiles: false, - addMainLanguageTranslationsToAllAssets: false, - translationsOutputFile: undefined, - skipPluralFormFunction: false - } - ); - - sinon.assert.notCalled( console.warn ); + expect( serveTranslations ).toHaveBeenCalledOnce(); + + expect( MultipleLanguageTranslationService ).toHaveBeenCalledOnce(); + expect( MultipleLanguageTranslationService ).toHaveBeenCalledWith( expect.objectContaining( { + mainLanguage: 'en', + compileAllLanguages: true, + additionalLanguages: [], + buildAllTranslationsToSeparateFiles: false, + addMainLanguageTranslationsToAllAssets: false, + translationsOutputFile: undefined, + skipPluralFormFunction: false + } ) ); + + expect( consoleWarnSpy ).not.toBeCalled(); } ); it( 'passes the skipPluralFormFunction option to the translation service', () => { @@ -220,21 +193,18 @@ describe( 'dev-translations/CKEditorTranslationsPlugin', () => { const ckEditorTranslationsPlugin = new CKEditorTranslationsPlugin( options ); ckEditorTranslationsPlugin.apply( {} ); - sinon.assert.calledOnce( stubs.serveTranslations ); - - sinon.assert.calledOnce( stubs.MultipleLanguageTranslationService ); - sinon.assert.calledWithExactly( - stubs.MultipleLanguageTranslationService, - { - mainLanguage: 'pl', - compileAllLanguages: false, - additionalLanguages: [], - buildAllTranslationsToSeparateFiles: false, - addMainLanguageTranslationsToAllAssets: false, - translationsOutputFile: undefined, - skipPluralFormFunction: true - } - ); + expect( serveTranslations ).toHaveBeenCalledOnce(); + + expect( MultipleLanguageTranslationService ).toHaveBeenCalledOnce(); + expect( MultipleLanguageTranslationService ).toHaveBeenCalledWith( expect.objectContaining( { + mainLanguage: 'pl', + compileAllLanguages: false, + additionalLanguages: [], + buildAllTranslationsToSeparateFiles: false, + addMainLanguageTranslationsToAllAssets: false, + translationsOutputFile: undefined, + skipPluralFormFunction: true + } ) ); } ); } ); diff --git a/packages/ckeditor5-dev-translations/tests/cleanpofilecontent.js b/packages/ckeditor5-dev-translations/tests/cleanpofilecontent.js index c7e02f95b..77a7d3774 100644 --- a/packages/ckeditor5-dev-translations/tests/cleanpofilecontent.js +++ b/packages/ckeditor5-dev-translations/tests/cleanpofilecontent.js @@ -3,11 +3,8 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const chai = require( 'chai' ); -const expect = chai.expect; -const cleanPoFileContent = require( '../lib/cleanpofilecontent' ); +import { describe, expect, it } from 'vitest'; +import cleanPoFileContent from '../lib/cleanpofilecontent.js'; describe( 'translations', () => { describe( 'cleanPoFileContent()', () => { diff --git a/packages/ckeditor5-dev-translations/tests/createdictionaryfrompofilecontent.js b/packages/ckeditor5-dev-translations/tests/createdictionaryfrompofilecontent.js index 050d552f5..2e243e57e 100644 --- a/packages/ckeditor5-dev-translations/tests/createdictionaryfrompofilecontent.js +++ b/packages/ckeditor5-dev-translations/tests/createdictionaryfrompofilecontent.js @@ -3,11 +3,8 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const chai = require( 'chai' ); -const expect = chai.expect; -const createDictionaryFromPoFileContent = require( '../lib/createdictionaryfrompofilecontent' ); +import { describe, expect, it } from 'vitest'; +import createDictionaryFromPoFileContent from '../lib/createdictionaryfrompofilecontent.js'; describe( 'translations', () => { describe( 'parsePoFileContent()', () => { diff --git a/packages/ckeditor5-dev-translations/tests/findmessages.js b/packages/ckeditor5-dev-translations/tests/findmessages.js index 268fc4d22..332cfe634 100644 --- a/packages/ckeditor5-dev-translations/tests/findmessages.js +++ b/packages/ckeditor5-dev-translations/tests/findmessages.js @@ -3,17 +3,14 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const findMessages = require( '../lib/findmessages' ); +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import traverse from '@babel/traverse'; describe( 'findMessages', () => { - const sandbox = sinon.createSandbox(); + let findMessages; - afterEach( () => { - sandbox.restore(); + beforeEach( async () => { + findMessages = ( await import( '../lib/findmessages.js' ) ).default; } ); it( 'should parse provided code and find messages from `t()` function calls on string literals', () => { @@ -216,4 +213,37 @@ describe( 'findMessages', () => { 'First t() call argument should be a string literal or an object literal (foo.js).' ] ); } ); + + describe( 'a non-type=module project support', () => { + beforeEach( async () => { + vi.resetAllMocks(); + vi.clearAllMocks(); + vi.resetModules(); + + vi.doMock( '@babel/traverse', () => ( { + default: { + default: traverse + } + } ) ); + + findMessages = ( await import( '../lib/findmessages.js' ) ).default; + } ); + + it( 'should parse provided code and find messages from `t()` function calls on string literals', () => { + const messages = []; + + findMessages( + `function x() { + const t = this.t; + t( 'Image' ); + t( 'CKEditor' ); + g( 'Some other function' ); + }`, + 'foo.js', + message => messages.push( message ) + ); + + expect( messages ).to.deep.equal( [ { id: 'Image', string: 'Image' }, { id: 'CKEditor', string: 'CKEditor' } ] ); + } ); + } ); } ); diff --git a/packages/ckeditor5-dev-translations/tests/multiplelanguagetranslationservice.js b/packages/ckeditor5-dev-translations/tests/multiplelanguagetranslationservice.js index 2a448c85c..a1489fc37 100644 --- a/packages/ckeditor5-dev-translations/tests/multiplelanguagetranslationservice.js +++ b/packages/ckeditor5-dev-translations/tests/multiplelanguagetranslationservice.js @@ -5,16 +5,22 @@ /* eslint-disable no-eval */ -'use strict'; - -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const path = require( 'path' ); -const proxyquire = require( 'proxyquire' ); +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import path from 'path'; +import fs from 'fs'; +import MultipleLanguageTranslationService from '../lib/multiplelanguagetranslationservice.js'; + +vi.mock( 'fs', () => ( { + default: { + existsSync: vi.fn(), + readFileSync: vi.fn(), + readdirSync: vi.fn() + } +} ) ); describe( 'translations', () => { describe( 'MultipleLanguageTranslationService', () => { - let MultipleLanguageTranslationService, stubs, filesAndDirs, fileContents, dirContents; + let filesAndDirs, fileContents, dirContents; let window; beforeEach( () => { @@ -22,25 +28,13 @@ describe( 'translations', () => { fileContents = {}; dirContents = {}; - stubs = { - fs: { - existsSync: path => filesAndDirs.includes( path ), - readFileSync: path => fileContents[ path ], - readdirSync: dir => dirContents[ dir ] - } - }; - - MultipleLanguageTranslationService = proxyquire( '../lib/multiplelanguagetranslationservice', { - 'fs': stubs.fs - } ); + vi.mocked( fs.existsSync ).mockImplementation( path => filesAndDirs.includes( path ) ); + vi.mocked( fs.readFileSync ).mockImplementation( path => fileContents[ path ] ); + vi.mocked( fs.readdirSync ).mockImplementation( dir => dirContents[ dir ] ); window = {}; } ); - afterEach( () => { - sinon.restore(); - } ); - describe( 'constructor()', () => { it( 'should initialize `SingleLanguageTranslationService`', () => { const translationService = new MultipleLanguageTranslationService( { mainLanguage: 'en', additionalLanguages: [ 'pl' ] } ); @@ -95,7 +89,7 @@ describe( 'translations', () => { it( 'should load PO file from the package only once per language', () => { const translationService = new MultipleLanguageTranslationService( { mainLanguage: 'pl', additionalLanguages: [ 'de' ] } ); - const loadPoFileSpy = sinon.stub( translationService, '_loadPoFile' ); + const loadPoFileSpy = vi.spyOn( translationService, '_loadPoFile' ); const pathToTranslationsDirectory = path.join( 'pathToPackage', 'lang', 'translations' ); @@ -105,7 +99,7 @@ describe( 'translations', () => { translationService.loadPackage( 'pathToPackage' ); translationService.loadPackage( 'pathToPackage' ); - sinon.assert.calledTwice( loadPoFileSpy ); + expect( loadPoFileSpy ).toBeCalledTimes( 2 ); } ); it( 'should load all PO files for the current package and add languages to the language list', () => { @@ -447,7 +441,7 @@ describe( 'translations', () => { additionalLanguages: [ 'xxx' ] } ); - const spy = sinon.spy(); + const spy = vi.fn(); translationService.on( 'error', spy ); @@ -482,7 +476,7 @@ describe( 'translations', () => { additionalLanguages: [ 'xxx' ] } ); - const spy = sinon.spy(); + const spy = vi.fn(); translationService.on( 'error', spy ); @@ -525,7 +519,7 @@ describe( 'translations', () => { mainLanguage: 'pl', additionalLanguages: [ 'xxx' ] } ); - const spy = sinon.spy(); + const spy = vi.fn(); translationService.on( 'error', spy ); @@ -546,8 +540,8 @@ describe( 'translations', () => { compilationAssetNames: [ 'ckeditor.js' ] } ); - sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, 'No translation has been found for the xxx language.' ); + expect( spy ).toHaveBeenCalledOnce(); + expect( spy ).toBeCalledWith( 'No translation has been found for the xxx language.' ); } ); it( 'should emit an error if translations for the main language are missing', () => { @@ -556,7 +550,7 @@ describe( 'translations', () => { additionalLanguages: [ 'pl' ] } ); - const errorSpy = sinon.spy(); + const errorSpy = vi.fn(); translationService.on( 'error', errorSpy ); @@ -577,8 +571,8 @@ describe( 'translations', () => { compilationAssetNames: [ 'ckeditor.js' ] } ); - sinon.assert.calledOnce( errorSpy ); - sinon.assert.calledWithExactly( errorSpy, 'No translation has been found for the xxx language.' ); + expect( errorSpy ).toHaveBeenCalledOnce(); + expect( errorSpy ).toBeCalledWith( 'No translation has been found for the xxx language.' ); } ); it( 'should emit a warning if the translation is missing', () => { @@ -586,7 +580,7 @@ describe( 'translations', () => { mainLanguage: 'pl', additionalLanguages: [] } ); - const warningSpy = sinon.spy(); + const warningSpy = vi.fn(); translationService.on( 'warning', warningSpy ); @@ -608,13 +602,13 @@ describe( 'translations', () => { compilationAssetNames: [ 'ckeditor.js' ] } ); - sinon.assert.calledOnce( warningSpy ); - sinon.assert.calledWithExactly( warningSpy, 'A translation is missing for \'Save\' in the \'pl\' language.' ); + expect( warningSpy ).toHaveBeenCalledOnce(); + expect( warningSpy ).toBeCalledWith( 'A translation is missing for \'Save\' in the \'pl\' language.' ); } ); it( 'should emit an error when there are multiple JS assets', () => { const translationService = new MultipleLanguageTranslationService( { mainLanguage: 'pl', additionalLanguages: [] } ); - const errorSpy = sinon.spy(); + const errorSpy = vi.fn(); translationService.on( 'error', errorSpy ); @@ -635,8 +629,8 @@ describe( 'translations', () => { compilationAssetNames: [ 'ckeditor.js', 'ckeditor1.js' ] } ); - sinon.assert.calledOnce( errorSpy ); - expect( errorSpy.getCalls()[ 0 ] ).to.match( + expect( errorSpy ).toHaveBeenCalledOnce(); + expect( errorSpy.mock.calls[ 0 ] ).to.match( /Too many JS assets has been found during the compilation./ ); @@ -652,7 +646,7 @@ describe( 'translations', () => { addMainLanguageTranslationsToAllAssets: true } ); - const errorSpy = sinon.spy(); + const errorSpy = vi.fn(); translationService.on( 'error', errorSpy ); @@ -673,7 +667,7 @@ describe( 'translations', () => { compilationAssetNames: [ 'foo.js', 'bar.js' ] } ); - sinon.assert.notCalled( errorSpy ); + expect( errorSpy ).not.toHaveBeenCalled(); expect( assets ).to.have.length( 2 ); @@ -691,7 +685,7 @@ describe( 'translations', () => { buildAllTranslationsToSeparateFiles: true } ); - const errorSpy = sinon.spy(); + const errorSpy = vi.fn(); translationService.on( 'error', errorSpy ); @@ -712,7 +706,7 @@ describe( 'translations', () => { compilationAssetNames: [] } ); - sinon.assert.notCalled( errorSpy ); + expect( errorSpy ).not.toHaveBeenCalled(); expect( assets ).to.have.length( 0 ); } ); @@ -724,8 +718,8 @@ describe( 'translations', () => { translationsOutputFile: 'foo/bar' } ); - const errorSpy = sinon.spy(); - const warningSpy = sinon.spy(); + const errorSpy = vi.fn(); + const warningSpy = vi.fn(); translationService.on( 'error', errorSpy ); translationService.on( 'warning', warningSpy ); @@ -757,8 +751,8 @@ describe( 'translations', () => { expect( assets[ 0 ] ).to.have.property( 'outputBody' ); expect( assets[ 0 ].outputBody ).to.have.length.greaterThan( 0 ); - expect( warningSpy.called ).to.equal( false ); - expect( errorSpy.called ).to.equal( false ); + expect( warningSpy ).not.toHaveBeenCalled(); + expect( errorSpy ).not.toHaveBeenCalled(); } ); it( 'should emit all files to a file specified by the `translationsOutputFile` option when it is specified (as regexp)', () => { @@ -768,8 +762,8 @@ describe( 'translations', () => { translationsOutputFile: /app\.js/ } ); - const errorSpy = sinon.spy(); - const warningSpy = sinon.spy(); + const errorSpy = vi.fn(); + const warningSpy = vi.fn(); translationService.on( 'error', errorSpy ); translationService.on( 'warning', warningSpy ); @@ -802,8 +796,8 @@ describe( 'translations', () => { expect( assets[ 0 ] ).to.have.property( 'outputBody' ); expect( assets[ 0 ].outputBody ).to.have.length.greaterThan( 0 ); - expect( warningSpy.called ).to.equal( false ); - expect( errorSpy.called ).to.equal( false ); + expect( warningSpy ).not.toHaveBeenCalled(); + expect( errorSpy ).not.toHaveBeenCalled(); } ); it( 'should emit all files to a file specified by the `translationsOutputFile` option when it is specified (as func.)', () => { @@ -813,8 +807,8 @@ describe( 'translations', () => { translationsOutputFile: name => /app\.js/.test( name ) } ); - const errorSpy = sinon.spy(); - const warningSpy = sinon.spy(); + const errorSpy = vi.fn(); + const warningSpy = vi.fn(); translationService.on( 'error', errorSpy ); translationService.on( 'warning', warningSpy ); @@ -847,8 +841,8 @@ describe( 'translations', () => { expect( assets[ 0 ] ).to.have.property( 'outputBody' ); expect( assets[ 0 ].outputBody ).to.have.length.greaterThan( 0 ); - expect( warningSpy.called ).to.equal( false ); - expect( errorSpy.called ).to.equal( false ); + expect( warningSpy ).not.toHaveBeenCalled(); + expect( errorSpy ).not.toHaveBeenCalled(); } ); it( 'should use the `outputDirectory` option for translation assets generated as new files', () => { @@ -857,8 +851,8 @@ describe( 'translations', () => { additionalLanguages: [ 'en' ] } ); - const warningSpy = sinon.spy(); - const errorSpy = sinon.spy(); + const warningSpy = vi.fn(); + const errorSpy = vi.fn(); translationService.on( 'warning', warningSpy ); translationService.on( 'error', errorSpy ); @@ -886,8 +880,8 @@ describe( 'translations', () => { expect( assets[ 0 ].outputPath ).to.equal( 'ckeditor.js' ); expect( assets[ 1 ].outputPath ).to.equal( path.join( 'custom-lang-path', 'en.js' ) ); - expect( warningSpy.called ).to.equal( false ); - expect( errorSpy.called ).to.equal( false ); + expect( warningSpy ).not.toHaveBeenCalled(); + expect( errorSpy ).not.toHaveBeenCalled(); } ); it( diff --git a/packages/ckeditor5-dev-translations/tests/translatesourceloader.js b/packages/ckeditor5-dev-translations/tests/translatesourceloader.js index 3a055b2ef..1b277bf1e 100644 --- a/packages/ckeditor5-dev-translations/tests/translatesourceloader.js +++ b/packages/ckeditor5-dev-translations/tests/translatesourceloader.js @@ -3,37 +3,28 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { expect } = require( 'chai' ); -const sinon = require( 'sinon' ); -const translateSourceLoader = require( '../lib/translatesourceloader' ); +import { describe, expect, it, vi } from 'vitest'; +import translateSourceLoader from '../lib/translatesourceloader.js'; describe( 'dev-translations/translateSourceLoader()', () => { - const sandbox = sinon.createSandbox(); - - afterEach( () => { - sandbox.restore(); - } ); - it( 'should return translated code', () => { const ctx = { query: { - translateSource: sandbox.spy( () => 'output' ) + translateSource: vi.fn( () => 'output' ) }, resourcePath: 'file.js', - callback: sinon.stub() + callback: vi.fn() }; const map = {}; translateSourceLoader.call( ctx, 'Source', map ); - sinon.assert.calledOnce( ctx.query.translateSource ); - sinon.assert.calledWithExactly( ctx.query.translateSource, 'Source', 'file.js' ); + expect( ctx.query.translateSource ).toHaveBeenCalledOnce(); + expect( ctx.query.translateSource ).toHaveBeenCalledWith( 'Source', 'file.js' ); - expect( ctx.callback.calledOnce ).to.equal( true ); - expect( ctx.callback.firstCall.args[ 0 ] ).to.equal( null ); - expect( ctx.callback.firstCall.args[ 1 ] ).to.equal( 'output' ); - expect( ctx.callback.firstCall.args[ 2 ] ).to.equal( map ); + expect( ctx.callback ).toHaveBeenCalledOnce(); + expect( ctx.callback.mock.calls[ 0 ][ 0 ] ).to.equal( null ); + expect( ctx.callback.mock.calls[ 0 ][ 1 ] ).to.equal( 'output' ); + expect( ctx.callback.mock.calls[ 0 ][ 2 ] ).to.equal( map ); } ); } ); diff --git a/packages/ckeditor5-dev-translations/vitest.config.js b/packages/ckeditor5-dev-translations/vitest.config.js new file mode 100644 index 000000000..5ad784a28 --- /dev/null +++ b/packages/ckeditor5-dev-translations/vitest.config.js @@ -0,0 +1,23 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig( { + test: { + testTimeout: 10000, + restoreMocks: true, + include: [ + 'tests/**/*.js' + ], + coverage: { + provider: 'v8', + include: [ + 'lib/**' + ], + reporter: [ 'text', 'json', 'html', 'lcov' ] + } + } +} ); diff --git a/packages/ckeditor5-dev-utils/lib/builds/getdllpluginwebpackconfig.js b/packages/ckeditor5-dev-utils/lib/builds/getdllpluginwebpackconfig.js index 6df015019..9fe5dd88e 100644 --- a/packages/ckeditor5-dev-utils/lib/builds/getdllpluginwebpackconfig.js +++ b/packages/ckeditor5-dev-utils/lib/builds/getdllpluginwebpackconfig.js @@ -3,34 +3,32 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const fs = require( 'fs-extra' ); -const { CKEditorTranslationsPlugin } = require( '@ckeditor/ckeditor5-dev-translations' ); -const bundler = require( '../bundler' ); -const loaders = require( '../loaders' ); +import path from 'path'; +import fs from 'fs-extra'; +import { CKEditorTranslationsPlugin } from '@ckeditor/ckeditor5-dev-translations'; +import { getLicenseBanner } from '../bundler/index.js'; +import { getIconsLoader, getStylesLoader, getTypeScriptLoader } from '../loaders/index.js'; /** * Returns a webpack configuration that creates a bundle file for the specified package. Thanks to that, plugins exported * by the package can be added to DLL builds. * - * @param {Object} webpack - * @param {Function} webpack.BannerPlugin Plugin used to add text to the top of the file. - * @param {Function} webpack.DllReferencePlugin Plugin used to import DLLs with webpack. - * @param {Object} options - * @param {String} options.themePath An absolute path to the theme package. - * @param {String} options.packagePath An absolute path to the root directory of the package. - * @param {String} options.manifestPath An absolute path to the CKEditor 5 DLL manifest file. - * @param {String} [options.tsconfigPath] An absolute path to the TypeScript configuration file. - * @param {Boolean} [options.isDevelopmentMode=false] Whether to build a dev mode of the package. - * @returns {Object} + * @param {object} webpack + * @param {function} webpack.BannerPlugin Plugin used to add text to the top of the file. + * @param {function} webpack.DllReferencePlugin Plugin used to import DLLs with webpack. + * @param {object} options + * @param {string} options.themePath An absolute path to the theme package. + * @param {string} options.packagePath An absolute path to the root directory of the package. + * @param {string} options.manifestPath An absolute path to the CKEditor 5 DLL manifest file. + * @param {string} [options.tsconfigPath] An absolute path to the TypeScript configuration file. + * @param {boolean} [options.isDevelopmentMode=false] Whether to build a dev mode of the package. + * @returns {object} */ -module.exports = function getDllPluginWebpackConfig( webpack, options ) { +export default async function getDllPluginWebpackConfig( webpack, options ) { // Terser requires webpack. However, it's needed in runtime. To avoid the "Cannot find module 'webpack'" error, // let's load the Terser dependency when `getDllPluginWebpackConfig()` is executed. // See: https://github.com/ckeditor/ckeditor5/issues/13136. - const TerserPlugin = require( 'terser-webpack-plugin' ); + const TerserPlugin = ( await import( 'terser-webpack-plugin' ) ).default; const { name: packageName } = fs.readJsonSync( path.join( options.packagePath, 'package.json' ) ); const langDirExists = fs.existsSync( path.join( options.packagePath, 'lang' ) ); @@ -58,11 +56,11 @@ module.exports = function getDllPluginWebpackConfig( webpack, options ) { plugins: [ new webpack.BannerPlugin( { - banner: bundler.getLicenseBanner(), + banner: getLicenseBanner(), raw: true } ), new webpack.DllReferencePlugin( { - manifest: require( options.manifestPath ), + manifest: fs.readJsonSync( options.manifestPath ), scope: 'ckeditor5/src', name: 'CKEditor5.dll' } ) @@ -77,12 +75,12 @@ module.exports = function getDllPluginWebpackConfig( webpack, options ) { module: { rules: [ - loaders.getIconsLoader( { matchExtensionOnly: true } ), - loaders.getStylesLoader( { + getIconsLoader( { matchExtensionOnly: true } ), + getStylesLoader( { themePath: options.themePath, minify: true } ), - loaders.getTypeScriptLoader( { + getTypeScriptLoader( { configFile: options.tsconfigPath || 'tsconfig.json' } ) ] @@ -127,14 +125,14 @@ module.exports = function getDllPluginWebpackConfig( webpack, options ) { } return webpackConfig; -}; +} /** * Transforms the package name (`@ckeditor/ckeditor5-foo-bar`) to the name that will be used while * exporting the library into the global scope. * - * @param {String} packageName - * @returns {String} + * @param {string} packageName + * @returns {string} */ function getGlobalKeyForPackage( packageName ) { return packageName @@ -146,7 +144,7 @@ function getGlobalKeyForPackage( packageName ) { * Extracts the main file name from the package name. * * @param packageName - * @returns {String} + * @returns {string} */ function getIndexFileName( packageName ) { return packageName.replace( /^@ckeditor\/ckeditor5?-/, '' ) + '.js'; diff --git a/packages/ckeditor5-dev-utils/lib/builds/index.js b/packages/ckeditor5-dev-utils/lib/builds/index.js index 4519c5e3d..d3a5f8a80 100644 --- a/packages/ckeditor5-dev-utils/lib/builds/index.js +++ b/packages/ckeditor5-dev-utils/lib/builds/index.js @@ -3,8 +3,4 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -module.exports = { - getDllPluginWebpackConfig: require( './getdllpluginwebpackconfig' ) -}; +export { default as getDllPluginWebpackConfig } from './getdllpluginwebpackconfig.js'; diff --git a/packages/ckeditor5-dev-utils/lib/bundler/createentryfile.js b/packages/ckeditor5-dev-utils/lib/bundler/createentryfile.js deleted file mode 100644 index 569948e8a..000000000 --- a/packages/ckeditor5-dev-utils/lib/bundler/createentryfile.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const fs = require( 'fs' ); -const getPlugins = require( './getplugins' ); -const getEditorConfig = require( './geteditorconfig' ); - -/** - * Generates an entry file which can be compiled by bundler, e.g. Webpack or Rollup. - * - * @param {String} destinationPath A path where entry file will be saved. - * @param {Object} options - * @param {Array.} options.plugins An array with paths to the plugins for the editor. - * @param {String} options.moduleName Name of exported UMD module. - * @param {String} options.editor A path to class which defined the editor. - * @param {Object} options.config Additional editor's configuration which will be built-in. - */ -module.exports = function createEntryFile( destinationPath, options ) { - const entryFileContent = renderEntryFile( options ); - - fs.writeFileSync( destinationPath, entryFileContent ); -}; - -function renderEntryFile( options ) { - const plugins = getPlugins( options.plugins ); - const date = new Date(); - - let content = `/** - * @license Copyright (c) 2003-${ date.getFullYear() }, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import ${ options.moduleName }Base from '${ options.editor }'; -`; - - for ( const pluginName of Object.keys( plugins ) ) { - content += `import ${ pluginName } from '${ plugins[ pluginName ] }';\n`; - } - - content += ` -export default class ${ options.moduleName } extends ${ options.moduleName }Base {} - -${ options.moduleName }.build = { - plugins: [ - ${ Object.keys( plugins ).join( ',\n\t\t' ) } - ], - config: ${ getEditorConfig( options.config ) } -}; -`; - - return content; -} diff --git a/packages/ckeditor5-dev-utils/lib/bundler/geteditorconfig.js b/packages/ckeditor5-dev-utils/lib/bundler/geteditorconfig.js deleted file mode 100644 index 44968d929..000000000 --- a/packages/ckeditor5-dev-utils/lib/bundler/geteditorconfig.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const javascriptStringify = require( 'javascript-stringify' ); - -/** - * Transforms specified configuration to a string that match to our code style. - * - * @param {Object} config - * @returns {String} - */ -module.exports = function getEditorConfig( config ) { - if ( !config ) { - return '{}'; - } - - return javascriptStringify( config, null, '\t' ) - // Indent all but the first line (so it can be easily concatenated with `config = ${ editorConfig }`). - .replace( /\n/g, '\n\t' ); -}; diff --git a/packages/ckeditor5-dev-utils/lib/bundler/getlicensebanner.js b/packages/ckeditor5-dev-utils/lib/bundler/getlicensebanner.js index e9d4ad409..a826c3273 100644 --- a/packages/ckeditor5-dev-utils/lib/bundler/getlicensebanner.js +++ b/packages/ckeditor5-dev-utils/lib/bundler/getlicensebanner.js @@ -3,9 +3,7 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -module.exports = function getLicenseBanner() { +export default function getLicenseBanner() { const date = new Date(); // License banner starts with `!`. That combines with uglifyjs' `comments` /^!/ option @@ -20,4 +18,4 @@ module.exports = function getLicenseBanner() { */` ); /* eslint-enable indent */ -}; +} diff --git a/packages/ckeditor5-dev-utils/lib/bundler/getplugins.js b/packages/ckeditor5-dev-utils/lib/bundler/getplugins.js deleted file mode 100644 index dc7d99246..000000000 --- a/packages/ckeditor5-dev-utils/lib/bundler/getplugins.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const path = require( 'path' ); - -/** - * Transforms specified an array of plugin paths to an object contains plugin names - * and paths to the plugins. - * - * Names of plugins returned in the object will be always unique. - * - * In case of two ore more plugins will have the same name: - * - typing => TypingPlugin - * - path/to/other/plugin/typing => Typing1Plugin - * - * @param {Array.} pluginPaths - * @returns {Object} - */ -module.exports = function getPlugins( pluginPaths ) { - const plugins = {}; - - pluginPaths.forEach( pathToFile => { - const basePluginName = capitalize( path.basename( pathToFile, '.js' ) ); - let pluginName = basePluginName + 'Plugin'; - let i = 0; - - while ( pluginName in plugins ) { - pluginName = basePluginName + ( ++i ).toString() + 'Plugin'; - } - - plugins[ pluginName ] = pathToFile; - } ); - - return plugins; -}; - -function capitalize( string ) { - return string.charAt( 0 ).toUpperCase() + string.slice( 1 ); -} diff --git a/packages/ckeditor5-dev-utils/lib/bundler/index.js b/packages/ckeditor5-dev-utils/lib/bundler/index.js index f097c1388..df7bf8b38 100644 --- a/packages/ckeditor5-dev-utils/lib/bundler/index.js +++ b/packages/ckeditor5-dev-utils/lib/bundler/index.js @@ -3,9 +3,4 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -module.exports = { - createEntryFile: require( './createentryfile' ), - getLicenseBanner: require( './getlicensebanner' ) -}; +export { default as getLicenseBanner } from './getlicensebanner.js'; diff --git a/packages/ckeditor5-dev-utils/lib/index.js b/packages/ckeditor5-dev-utils/lib/index.js index 12dfc2017..c3360defe 100644 --- a/packages/ckeditor5-dev-utils/lib/index.js +++ b/packages/ckeditor5-dev-utils/lib/index.js @@ -3,14 +3,10 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -module.exports = { - logger: require( './logger' ), - tools: require( './tools' ), - loaders: require( './loaders' ), - stream: require( './stream' ), - bundler: require( './bundler/index' ), - builds: require( './builds/index' ), - styles: require( './styles/index' ) -}; +export { default as logger } from './logger/index.js'; +export * as builds from './builds/index.js'; +export * as bundler from './bundler/index.js'; +export * as loaders from './loaders/index.js'; +export * as stream from './stream/index.js'; +export * as styles from './styles/index.js'; +export * as tools from './tools/index.js'; diff --git a/packages/ckeditor5-dev-utils/lib/loaders/ck-debug-loader.js b/packages/ckeditor5-dev-utils/lib/loaders/ck-debug-loader.js index 4e5c91f14..41e28ab8a 100644 --- a/packages/ckeditor5-dev-utils/lib/loaders/ck-debug-loader.js +++ b/packages/ckeditor5-dev-utils/lib/loaders/ck-debug-loader.js @@ -9,10 +9,10 @@ * E.g. if the `CK_DEBUG_ENGINE` flag is set to true, then all lines starting with * `// @if CK_DEBUG_ENGINE //` will be uncommented. * - * @param {String} source + * @param {string} source * @param {any} map */ -module.exports = function ckDebugLoader( source, map ) { +export default function ckDebugLoader( source, map ) { source = source.replace( /\/\/ @if (!?[\w]+) \/\/(.+)/g, ( match, flagName, body ) => { // `this.query` comes from the webpack loader configuration specified as the loader options. // { @@ -32,4 +32,4 @@ module.exports = function ckDebugLoader( source, map ) { } ); this.callback( null, source, map ); -}; +} diff --git a/packages/ckeditor5-dev-utils/lib/loaders/getcoverageloader.js b/packages/ckeditor5-dev-utils/lib/loaders/getcoverageloader.js new file mode 100644 index 000000000..e2c46f001 --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/loaders/getcoverageloader.js @@ -0,0 +1,69 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import path from 'path'; + +const escapedPathSep = path.sep == '/' ? '/' : '\\\\'; + +/** + * @param {object} options] + * @param {Array.} options.files + * @returns {object} + */ +export default function getCoverageLoader( { files } ) { + return { + test: /\.[jt]s$/, + use: [ + { + loader: 'babel-loader', + options: { + plugins: [ + 'babel-plugin-istanbul' + ] + } + } + ], + include: getPathsToIncludeForCoverage( files ), + exclude: [ + new RegExp( `${ escapedPathSep }(lib)${ escapedPathSep }` ) + ] + }; +} + +/** + * Returns an array of `/ckeditor5-name\/src\//` regexps based on passed globs. + * E.g., `ckeditor5-utils/**\/*.js` will be converted to `/ckeditor5-utils\/src/`. + * + * This loose way of matching packages for CC works with packages under various paths. + * E.g., `workspace/ckeditor5-utils` and `ckeditor5/node_modules/ckeditor5-utils` and every other path. + * + * @param {Array.} globs + * @returns {Array.} + */ +function getPathsToIncludeForCoverage( globs ) { + const values = globs + .reduce( ( returnedPatterns, globPatterns ) => { + returnedPatterns.push( ...globPatterns ); + + return returnedPatterns; + }, [] ) + .map( glob => { + const matchCKEditor5 = glob.match( /\/(ckeditor5-[^/]+)\/(?!.*ckeditor5-)/ ); + + if ( matchCKEditor5 ) { + const packageName = matchCKEditor5[ 1 ] + // A special case when --files='!engine' or --files='!engine|ui' was passed. + // Convert it to /ckeditor5-(?!engine)[^/]\/src\//. + .replace( /ckeditor5-!\(([^)]+)\)\*/, 'ckeditor5-(?!$1)[^' + escapedPathSep + ']+' ) + .replace( 'ckeditor5-*', 'ckeditor5-[a-z]+' ); + + return new RegExp( packageName + escapedPathSep + 'src' + escapedPathSep ); + } + } ) + // Filter undefined ones. + .filter( path => path ); + + return [ ...new Set( values ) ]; +} diff --git a/packages/ckeditor5-dev-utils/lib/loaders/getdebugloader.js b/packages/ckeditor5-dev-utils/lib/loaders/getdebugloader.js new file mode 100644 index 000000000..4f514fb23 --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/loaders/getdebugloader.js @@ -0,0 +1,21 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = path.dirname( __filename ); + +/** + * @param {Array.} debugFlags + * @returns {object} + */ +export default function getDebugLoader( debugFlags ) { + return { + loader: path.join( __dirname, 'ck-debug-loader.js' ), + options: { debugFlags } + }; +} diff --git a/packages/ckeditor5-dev-utils/lib/loaders/getformattedtextloader.js b/packages/ckeditor5-dev-utils/lib/loaders/getformattedtextloader.js new file mode 100644 index 000000000..e4c561964 --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/loaders/getformattedtextloader.js @@ -0,0 +1,14 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @returns {object} + */ +export default function getFormattedTextLoader() { + return { + test: /\.(txt|html|rtf)$/, + use: [ 'raw-loader' ] + }; +} diff --git a/packages/ckeditor5-dev-utils/lib/loaders/geticonsloader.js b/packages/ckeditor5-dev-utils/lib/loaders/geticonsloader.js new file mode 100644 index 000000000..73038923d --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/loaders/geticonsloader.js @@ -0,0 +1,16 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @param {object} [options] + * @param {boolean} [options.matchExtensionOnly] + * @returns {object} + */ +export default function getIconsLoader( { matchExtensionOnly = false } = {} ) { + return { + test: matchExtensionOnly ? /\.svg$/ : /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/, + use: [ 'raw-loader' ] + }; +} diff --git a/packages/ckeditor5-dev-utils/lib/loaders/getjavascriptloader.js b/packages/ckeditor5-dev-utils/lib/loaders/getjavascriptloader.js new file mode 100644 index 000000000..85e800575 --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/loaders/getjavascriptloader.js @@ -0,0 +1,18 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import getDebugLoader from './getdebugloader.js'; + +/** + * @param {object} options + * @param {Array.} options.debugFlags + * @returns {object} + */ +export default function getJavaScriptLoader( { debugFlags } ) { + return { + test: /\.js$/, + ...getDebugLoader( debugFlags ) + }; +} diff --git a/packages/ckeditor5-dev-utils/lib/loaders/getstylesloader.js b/packages/ckeditor5-dev-utils/lib/loaders/getstylesloader.js new file mode 100644 index 000000000..c4e4925ba --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/loaders/getstylesloader.js @@ -0,0 +1,58 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import { getPostCssConfig } from '../styles/index.js'; + +/** + * @param {object} options + * @param {string} options.themePath + * @param {boolean} [options.minify] + * @param {boolean} [options.sourceMap] + * @param {boolean} [options.extractToSeparateFile] + * @param {boolean} [options.skipPostCssLoader] + * @returns {object} + */ +export default function getStylesLoader( options ) { + const { + themePath, + minify = false, + sourceMap = false, + extractToSeparateFile = false, + skipPostCssLoader = false + } = options; + + const getBundledLoader = () => ( { + loader: 'style-loader', + options: { + injectType: 'singletonStyleTag', + attributes: { + 'data-cke': true + } + } + } ); + + const getExtractedLoader = () => { + return MiniCssExtractPlugin.loader; + }; + + return { + test: /\.css$/, + use: [ + extractToSeparateFile ? getExtractedLoader() : getBundledLoader(), + 'css-loader', + skipPostCssLoader ? null : { + loader: 'postcss-loader', + options: { + postcssOptions: getPostCssConfig( { + themeImporter: { themePath }, + minify, + sourceMap + } ) + } + } + ].filter( Boolean ) + }; +} diff --git a/packages/ckeditor5-dev-utils/lib/loaders/gettypescriptloader.js b/packages/ckeditor5-dev-utils/lib/loaders/gettypescriptloader.js new file mode 100644 index 000000000..c46d8b3f7 --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/loaders/gettypescriptloader.js @@ -0,0 +1,35 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import getDebugLoader from './getdebugloader.js'; + +/** + * @param {object} [options] + * @param {string} [options.configFile] + * @param {Array.} [options.debugFlags] + * @param {boolean} [options.includeDebugLoader] + * @returns {object} + */ +export default function getTypeScriptLoader( options = {} ) { + const { + configFile = 'tsconfig.json', + debugFlags = [], + includeDebugLoader = false + } = options; + + return { + test: /\.ts$/, + use: [ + { + loader: 'esbuild-loader', + options: { + target: 'es2022', + tsconfig: configFile + } + }, + includeDebugLoader ? getDebugLoader( debugFlags ) : null + ].filter( Boolean ) + }; +} diff --git a/packages/ckeditor5-dev-utils/lib/loaders/index.js b/packages/ckeditor5-dev-utils/lib/loaders/index.js index 553189dfb..507823e7b 100644 --- a/packages/ckeditor5-dev-utils/lib/loaders/index.js +++ b/packages/ckeditor5-dev-utils/lib/loaders/index.js @@ -3,198 +3,10 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); -const { getPostCssConfig } = require( '../styles' ); - -const escapedPathSep = path.sep == '/' ? '/' : '\\\\'; - -module.exports = { - /** - * @param {Object} [options] - * @param {String} [options.configFile] - * @param {Array.} [options.debugFlags] - * @param {Boolean} [options.includeDebugLoader] - * @returns {Object} - */ - getTypeScriptLoader( options = {} ) { - const { - configFile = 'tsconfig.json', - debugFlags = [], - includeDebugLoader = false - } = options; - - return { - test: /\.ts$/, - use: [ - { - loader: 'esbuild-loader', - options: { - target: 'es2022', - tsconfig: configFile - } - }, - includeDebugLoader ? getDebugLoader( debugFlags ) : null - ].filter( Boolean ) - }; - }, - - /** - * @param {Object} options - * @param {Array.} options.debugFlags - * @returns {Object} - */ - getJavaScriptLoader( { debugFlags } ) { - return { - test: /\.js$/, - ...getDebugLoader( debugFlags ) - }; - }, - - /** - * @param {Object} options - * @param {String} options.themePath - * @param {Boolean} [options.minify] - * @param {Boolean} [options.sourceMap] - * @param {Boolean} [options.extractToSeparateFile] - * @param {Boolean} [options.skipPostCssLoader] - * @returns {Object} - */ - getStylesLoader( options ) { - const { - themePath, - minify = false, - sourceMap = false, - extractToSeparateFile = false, - skipPostCssLoader = false - } = options; - - const getBundledLoader = () => ( { - loader: 'style-loader', - options: { - injectType: 'singletonStyleTag', - attributes: { - 'data-cke': true - } - } - } ); - - const getExtractedLoader = () => { - return MiniCssExtractPlugin.loader; - }; - - return { - test: /\.css$/, - use: [ - extractToSeparateFile ? getExtractedLoader() : getBundledLoader(), - 'css-loader', - skipPostCssLoader ? null : { - loader: 'postcss-loader', - options: { - postcssOptions: getPostCssConfig( { - themeImporter: { themePath }, - minify, - sourceMap - } ) - } - } - ].filter( Boolean ) - }; - }, - - /** - * @param {Object} [options] - * @param {Boolean} [options.matchExtensionOnly] - * @returns {Object} - */ - getIconsLoader( { matchExtensionOnly = false } = {} ) { - return { - test: matchExtensionOnly ? /\.svg$/ : /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/, - use: [ 'raw-loader' ] - }; - }, - - /** - * @returns {Object} - */ - getFormattedTextLoader() { - return { - test: /\.(txt|html|rtf)$/, - use: [ 'raw-loader' ] - }; - }, - - /** - * @param {Object} options] - * @param {Array.} options.files - * @returns {Object} - */ - getCoverageLoader( { files } ) { - return { - test: /\.[jt]s$/, - use: [ - { - loader: 'babel-loader', - options: { - plugins: [ - 'babel-plugin-istanbul' - ] - } - } - ], - include: getPathsToIncludeForCoverage( files ), - exclude: [ - new RegExp( `${ escapedPathSep }(lib)${ escapedPathSep }` ) - ] - }; - } -}; - -/** - * @param {Array.} debugFlags - * @returns {Object} - */ -function getDebugLoader( debugFlags ) { - return { - loader: path.join( __dirname, 'ck-debug-loader' ), - options: { debugFlags } - }; -} - -/** - * Returns an array of `/ckeditor5-name\/src\//` regexps based on passed globs. - * E.g., `ckeditor5-utils/**\/*.js` will be converted to `/ckeditor5-utils\/src/`. - * - * This loose way of matching packages for CC works with packages under various paths. - * E.g., `workspace/ckeditor5-utils` and `ckeditor5/node_modules/ckeditor5-utils` and every other path. - * - * @param {Array.} globs - * @returns {Array.} - */ -function getPathsToIncludeForCoverage( globs ) { - const values = globs - .reduce( ( returnedPatterns, globPatterns ) => { - returnedPatterns.push( ...globPatterns ); - - return returnedPatterns; - }, [] ) - .map( glob => { - const matchCKEditor5 = glob.match( /\/(ckeditor5-[^/]+)\/(?!.*ckeditor5-)/ ); - - if ( matchCKEditor5 ) { - const packageName = matchCKEditor5[ 1 ] - // A special case when --files='!engine' or --files='!engine|ui' was passed. - // Convert it to /ckeditor5-(?!engine)[^/]\/src\//. - .replace( /ckeditor5-!\(([^)]+)\)\*/, 'ckeditor5-(?!$1)[^' + escapedPathSep + ']+' ) - .replace( 'ckeditor5-*', 'ckeditor5-[a-z]+' ); - - return new RegExp( packageName + escapedPathSep + 'src' + escapedPathSep ); - } - } ) - // Filter undefined ones. - .filter( path => path ); - - return [ ...new Set( values ) ]; -} +export { default as getCoverageLoader } from './getcoverageloader.js'; +export { default as getTypeScriptLoader } from './gettypescriptloader.js'; +export { default as getDebugLoader } from './getdebugloader.js'; +export { default as getIconsLoader } from './geticonsloader.js'; +export { default as getFormattedTextLoader } from './getformattedtextloader.js'; +export { default as getJavaScriptLoader } from './getjavascriptloader.js'; +export { default as getStylesLoader } from './getstylesloader.js'; diff --git a/packages/ckeditor5-dev-utils/lib/logger.js b/packages/ckeditor5-dev-utils/lib/logger/index.js similarity index 83% rename from packages/ckeditor5-dev-utils/lib/logger.js rename to packages/ckeditor5-dev-utils/lib/logger/index.js index 1cdf69a6f..d180cfb1d 100644 --- a/packages/ckeditor5-dev-utils/lib/logger.js +++ b/packages/ckeditor5-dev-utils/lib/logger/index.js @@ -3,9 +3,8 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import chalk from 'chalk'; -const chalk = require( 'chalk' ); const levels = new Map(); // Displays everything. @@ -27,7 +26,7 @@ levels.set( 'error', new Set( [ 'info', 'warning', 'error' ] ) ); * * Usage: * - * const logger = require( '@ckeditor/ckeditor5-dev-utils' ).logger; + * import { logger } from '@ckeditor/ckeditor5-dev-utils'; * * const infoLog = logger( 'info' ); * infoLog.info( 'Message.' ); // This message will be always displayed. @@ -46,18 +45,18 @@ levels.set( 'error', new Set( [ 'info', 'warning', 'error' ] ) ); * * Additionally, the `logger#error()` method prints the error instance if provided as the second argument. * - * @param {String} moduleVerbosity='info' Level of the verbosity for all log methods. - * @returns {Object} logger + * @param {string} moduleVerbosity='info' Level of the verbosity for all log methods. + * @returns {object} logger * @returns {Function} logger.info * @returns {Function} logger.warning * @returns {Function} logger.error */ -module.exports = ( moduleVerbosity = 'info' ) => { +export default function logger( moduleVerbosity = 'info' ) { return { /** * Displays a message when verbosity level is equal to 'info'. * - * @param {String} message Message to log. + * @param {string} message Message to log. */ info( message ) { this._log( 'info', message ); @@ -66,7 +65,7 @@ module.exports = ( moduleVerbosity = 'info' ) => { /** * Displays a warning message when verbosity level is equal to 'info' or 'warning'. * - * @param {String} message Message to log. + * @param {string} message Message to log. */ warning( message ) { this._log( 'warning', chalk.yellow( message ) ); @@ -75,7 +74,7 @@ module.exports = ( moduleVerbosity = 'info' ) => { /** * Displays an error message. * - * @param {String} message Message to log. + * @param {string} message Message to log. * @param {Error} [error] An error instance to log in the console. */ error( message, error ) { @@ -84,8 +83,8 @@ module.exports = ( moduleVerbosity = 'info' ) => { /** * @private - * @param {String} messageVerbosity Verbosity of particular message. - * @param {String} message Message to log. + * @param {string} messageVerbosity Verbosity of particular message. + * @param {string} message Message to log. * @param {Error} [error] An error instance to log in the console. */ _log( messageVerbosity, message, error ) { @@ -100,4 +99,4 @@ module.exports = ( moduleVerbosity = 'info' ) => { } } }; -}; +} diff --git a/packages/ckeditor5-dev-utils/lib/stream.js b/packages/ckeditor5-dev-utils/lib/stream.js deleted file mode 100644 index cd44129f1..000000000 --- a/packages/ckeditor5-dev-utils/lib/stream.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const path = require( 'path' ); -const PassThrough = require( 'stream' ).PassThrough; -const through = require( 'through2' ); - -const stream = { - /** - * Creates a simple duplex stream. - * - * @param {Function} [callback] A callback which will be executed with each chunk. - * The callback can return a Promise to perform async actions before other chunks are accepted. - * @returns {Stream} - */ - noop( callback ) { - if ( !callback ) { - return new PassThrough( { objectMode: true } ); - } - - return through( { objectMode: true }, ( chunk, encoding, throughCallback ) => { - const callbackResult = callback( chunk ); - - if ( callbackResult instanceof Promise ) { - callbackResult - .then( () => { - throughCallback( null, chunk ); - } ) - .catch( err => { - throughCallback( err ); - } ); - } else { - throughCallback( null, chunk ); - } - } ); - }, - - /** - * Checks whether a file is a test file. - * - * @param {Vinyl} file - * @returns {Boolean} - */ - isTestFile( file ) { - // TODO this should be based on bender configuration (config.tests.*.paths). - if ( !file.relative.startsWith( 'tests' + path.sep ) ) { - return false; - } - - const dirFrags = file.relative.split( path.sep ); - - return !dirFrags.some( dirFrag => { - return dirFrag.startsWith( '_' ) && dirFrag != '_utils-tests'; - } ); - }, - - /** - * Checks whether a file is a source file. - * - * @param {Vinyl} file - * @returns {Boolean} - */ - isSourceFile( file ) { - return !stream.isTestFile( file ); - }, - - /** - * Checks whether a file is a JS file. - * - * @param {Vinyl} file - * @returns {Boolean} - */ - isJSFile( file ) { - return file.path.endsWith( '.js' ); - } -}; - -module.exports = stream; diff --git a/packages/ckeditor5-dev-utils/lib/stream/index.js b/packages/ckeditor5-dev-utils/lib/stream/index.js new file mode 100644 index 000000000..e843be3ef --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/stream/index.js @@ -0,0 +1,6 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +export { default as noop } from './noop.js'; diff --git a/packages/ckeditor5-dev-utils/lib/stream/noop.js b/packages/ckeditor5-dev-utils/lib/stream/noop.js new file mode 100644 index 000000000..8388a51fd --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/stream/noop.js @@ -0,0 +1,29 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { PassThrough } from 'stream'; +import through from 'through2'; + +export default function noop( callback ) { + if ( !callback ) { + return new PassThrough( { objectMode: true } ); + } + + return through( { objectMode: true }, ( chunk, encoding, throughCallback ) => { + const callbackResult = callback( chunk ); + + if ( callbackResult instanceof Promise ) { + callbackResult + .then( () => { + throughCallback( null, chunk ); + } ) + .catch( err => { + throughCallback( err ); + } ); + } else { + throughCallback( null, chunk ); + } + } ); +} diff --git a/packages/ckeditor5-dev-utils/lib/styles/getpostcssconfig.js b/packages/ckeditor5-dev-utils/lib/styles/getpostcssconfig.js index 136037ca9..16a6ac71a 100644 --- a/packages/ckeditor5-dev-utils/lib/styles/getpostcssconfig.js +++ b/packages/ckeditor5-dev-utils/lib/styles/getpostcssconfig.js @@ -3,31 +3,37 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /* eslint-env node */ +import postCssImport from 'postcss-import'; +import postCssMixins from 'postcss-mixins'; +import postCssNesting from 'postcss-nesting'; +import cssnano from 'cssnano'; +import themeLogger from './themelogger.js'; +import themeImporter from './themeimporter.js'; + /** * Returns a PostCSS configuration to build the editor styles (e.g. used by postcss-loader). * - * @param {Object} options - * @param {Boolean} options.sourceMap When true, an inline source map will be built into the output CSS. - * @param {Boolean} options.minify When true, the output CSS will be minified. + * @param {object} options + * @param {boolean} options.sourceMap When true, an inline source map will be built into the output CSS. + * @param {boolean} options.minify When true, the output CSS will be minified. * @param {ThemeImporterOptions} options.themeImporter Configuration of the theme-importer PostCSS plugin. * See the plugin to learn more. - * @returns {Object} A PostCSS configuration object, e.g. to be used by the postcss-loader. + * @returns {object} A PostCSS configuration object, e.g. to be used by the postcss-loader. */ -module.exports = function getPostCssConfig( options = {} ) { +export default function getPostCssConfig( options = {} ) { const config = { plugins: [ - require( 'postcss-import' )(), - require( './themeimporter' )( options.themeImporter ), - require( 'postcss-mixins' )(), - require( 'postcss-nesting' )( { + postCssImport(), + themeImporter( options.themeImporter ), + postCssMixins(), + postCssNesting( { // https://github.com/ckeditor/ckeditor5/issues/11730 - noIsPseudoSelector: true + noIsPseudoSelector: true, + edition: '2021' } ), - require( './themelogger' )() + themeLogger() ] }; @@ -36,7 +42,7 @@ module.exports = function getPostCssConfig( options = {} ) { } if ( options.minify ) { - config.plugins.push( require( 'cssnano' )( { + config.plugins.push( cssnano( { preset: 'default', autoprefixer: false, reduceIdents: false @@ -44,4 +50,4 @@ module.exports = function getPostCssConfig( options = {} ) { } return config; -}; +} diff --git a/packages/ckeditor5-dev-utils/lib/styles/index.js b/packages/ckeditor5-dev-utils/lib/styles/index.js index e8f1e70d5..05c612216 100644 --- a/packages/ckeditor5-dev-utils/lib/styles/index.js +++ b/packages/ckeditor5-dev-utils/lib/styles/index.js @@ -3,10 +3,5 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -module.exports = { - getPostCssConfig: require( './getpostcssconfig' ), - themeImporter: require( './themeimporter' ), - themeLogger: require( './themelogger' ) -}; +export { default as getPostCssConfig } from './getpostcssconfig.js'; +export { default as themeImporter } from './themeimporter.js'; diff --git a/packages/ckeditor5-dev-utils/lib/styles/themeimporter.js b/packages/ckeditor5-dev-utils/lib/styles/themeimporter.js index 106ac24ab..c6ea292e6 100644 --- a/packages/ckeditor5-dev-utils/lib/styles/themeimporter.js +++ b/packages/ckeditor5-dev-utils/lib/styles/themeimporter.js @@ -3,16 +3,18 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /* eslint-env node */ -const fs = require( 'fs' ); -const path = require( 'path' ); -const postcss = require( 'postcss' ); -const chalk = require( 'chalk' ); -const log = require( '../logger' )(); -const getPackageName = require( './utils/getpackagename' ); +import fs from 'fs'; +import path from 'path'; +import postcss from 'postcss'; +import postCssImport from 'postcss-import'; +import chalk from 'chalk'; +import logger from '../logger/index.js'; +import themeLogger from './themelogger.js'; +import getPackageName from './utils/getpackagename.js'; + +const log = logger(); /** * A PostCSS plugin that loads a theme files from specified path. @@ -38,7 +40,7 @@ const getPackageName = require( './utils/getpackagename' ); * @param {ThemeImporterOptions} pluginOptions * @returns {Function} A PostCSS plugin. */ -module.exports = ( pluginOptions = {} ) => { +function themeImporter( pluginOptions = {} ) { return { postcssPlugin: 'postcss-ckeditor5-theme-importer', Once( root, { result } ) { @@ -47,8 +49,8 @@ module.exports = ( pluginOptions = {} ) => { debug: pluginOptions.debug || false, postCssOptions: { plugins: [ - require( 'postcss-import' )(), - require( './themelogger' )() + postCssImport(), + themeLogger() ] }, root, result @@ -57,9 +59,11 @@ module.exports = ( pluginOptions = {} ) => { return importThemeFile( options ); } }; -}; +} + +themeImporter.postcss = true; -module.exports.postcss = true; +export default themeImporter; /** * Imports a complementary theme file corresponding with a CSS file being processed by @@ -154,9 +158,9 @@ function importFile( options ) { * `/foo/bar/ckeditor5-theme-foo/ckeditor5-qux/theme/components/button.css` * * @private - * @param {String} themePath Path to the theme. - * @param {String} inputFilePath Path to the CSS file which is to be themed. - * @returns {String} + * @param {string} themePath Path to the theme. + * @param {string} inputFilePath Path to the CSS file which is to be themed. + * @returns {string} */ function getThemeFilePath( themePath, inputFilePath ) { // ckeditor5-theme-foo/theme/theme.css -> ckeditor5-theme-foo/theme @@ -198,11 +202,11 @@ function getThemeFilePath( themePath, inputFilePath ) { * ... * } * - * @member {String} [ThemeImporterOptions#themePath] + * @member {string} [ThemeImporterOptions#themePath] */ /** * When `true` it enables debug logs in the console. * - * @member {String} [ThemeImporterOptions#debug=false] + * @member {string} [ThemeImporterOptions#debug=false] */ diff --git a/packages/ckeditor5-dev-utils/lib/styles/themelogger.js b/packages/ckeditor5-dev-utils/lib/styles/themelogger.js index e9fb67f43..9effaf283 100644 --- a/packages/ckeditor5-dev-utils/lib/styles/themelogger.js +++ b/packages/ckeditor5-dev-utils/lib/styles/themelogger.js @@ -3,21 +3,21 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /** * A plugin that prepends a path to the file in the comment for each file * processed by PostCSS. * * @returns {Function} A PostCSS plugin. */ -module.exports = () => { +function themeLogger() { return { postcssPlugin: 'postcss-ckeditor5-theme-logger', Once( root ) { root.prepend( `/* ${ root.source.input.file } */ \n` ); } }; -}; +} + +themeLogger.postcss = true; -module.exports.postcss = true; +export default themeLogger; diff --git a/packages/ckeditor5-dev-utils/lib/styles/utils/getpackagename.js b/packages/ckeditor5-dev-utils/lib/styles/utils/getpackagename.js index 1ea16e247..38e9596af 100644 --- a/packages/ckeditor5-dev-utils/lib/styles/utils/getpackagename.js +++ b/packages/ckeditor5-dev-utils/lib/styles/utils/getpackagename.js @@ -3,8 +3,6 @@ * For licensing, see LICENSE.md. */ -'use strict'; - /* eslint-env node */ /** @@ -30,10 +28,10 @@ * * "ckeditor5-bar" * - * @param {String} inputFilePath A path to the file. - * @returns {String} The name of the package. + * @param {string} inputFilePath A path to the file. + * @returns {string} The name of the package. */ -module.exports = function getPackageName( inputFilePath ) { +export default function getPackageName( inputFilePath ) { const match = inputFilePath.match( /^.+[/\\](ckeditor5-[^/\\]+)/ ); if ( match ) { @@ -41,4 +39,4 @@ module.exports = function getPackageName( inputFilePath ) { } else { return null; } -}; +} diff --git a/packages/ckeditor5-dev-utils/lib/tools.js b/packages/ckeditor5-dev-utils/lib/tools.js deleted file mode 100644 index 3fae17989..000000000 --- a/packages/ckeditor5-dev-utils/lib/tools.js +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const chalk = require( 'chalk' ); -const createSpinner = require( './tools/createspinner' ); - -module.exports = { - createSpinner, - - /** - * Executes a shell command. - * - * @param {String} command The command to be executed. - * @param {Object} options - * @param {'info'|'warning'|'error'|'silent'} [options.verbosity='info'] Level of the verbosity. If set as 'info' - * both outputs (stdout and stderr) will be logged. If set as 'error', only stderr output will be logged. - * @param {String} [options.cwd=process.cwd()] - * @param {Boolean} [options.async=false] If set, the command execution is asynchronous. The execution is synchronous by default. - * @returns {String|Promise.} The command output. - */ - shExec( command, options = {} ) { - const { - verbosity = 'info', - cwd = process.cwd(), - async = false - } = options; - - const logger = require( './logger' ); - const log = logger( verbosity ); - const sh = require( 'shelljs' ); - - sh.config.silent = true; - - const execOptions = { cwd }; - - if ( async ) { - return new Promise( ( resolve, reject ) => { - sh.exec( command, execOptions, ( code, stdout, stderr ) => { - try { - const result = execHandler( code, stdout, stderr ); - - resolve( result ); - } catch ( err ) { - reject( err ); - } - } ); - } ); - } - - const { code, stdout, stderr } = sh.exec( command, execOptions ); - - return execHandler( code, stdout, stderr ); - - function execHandler( code, stdout, stderr ) { - const grey = chalk.grey; - - if ( code ) { - if ( stdout ) { - log.error( grey( stdout ) ); - } - - if ( stderr ) { - log.error( grey( stderr ) ); - } - - throw new Error( `Error while executing ${ command }: ${ stderr }` ); - } - - if ( stdout ) { - log.info( grey( stdout ) ); - } - - if ( stderr ) { - log.info( grey( stderr ) ); - } - - return stdout; - } - }, - - /** - * Returns array with all directories under specified path. - * - * @param {String} path - * @returns {Array} - */ - getDirectories( path ) { - const fs = require( 'fs' ); - const pth = require( 'path' ); - - const isDirectory = path => { - try { - return fs.statSync( path ).isDirectory(); - } catch ( e ) { - return false; - } - }; - - return fs.readdirSync( path ).filter( item => { - return isDirectory( pth.join( path, item ) ); - } ); - }, - - /** - * Updates JSON file under specified path. - * @param {String} path Path to file on disk. - * @param {Function} updateFunction Function that will be called with parsed JSON object. It should return - * modified JSON object to save. - */ - updateJSONFile( path, updateFunction ) { - const fs = require( 'fs' ); - - const contents = fs.readFileSync( path, 'utf-8' ); - let json = JSON.parse( contents ); - json = updateFunction( json ); - - fs.writeFileSync( path, JSON.stringify( json, null, 2 ) + '\n', 'utf-8' ); - } -}; diff --git a/packages/ckeditor5-dev-utils/lib/tools/createspinner.js b/packages/ckeditor5-dev-utils/lib/tools/createspinner.js index 726903bd1..88c82d9e3 100644 --- a/packages/ckeditor5-dev-utils/lib/tools/createspinner.js +++ b/packages/ckeditor5-dev-utils/lib/tools/createspinner.js @@ -3,12 +3,10 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const readline = require( 'readline' ); -const isInteractive = require( 'is-interactive' ); -const cliSpinners = require( 'cli-spinners' ); -const cliCursor = require( 'cli-cursor' ); +import readline from 'readline'; +import isInteractive from 'is-interactive'; +import cliSpinners from 'cli-spinners'; +import cliCursor from 'cli-cursor'; // A size of default indent for a log. const INDENT_SIZE = 3; @@ -18,18 +16,18 @@ const INDENT_SIZE = 3; * * The spinner improves UX when processing a time-consuming task. A developer does not have to consider whether the process hanged on. * - * @param {String} title Description of the current processed task. - * @param {Object} [options={}] - * @param {Boolean} [options.isDisabled] Whether the spinner should be disabled. - * @param {String} [options.emoji='📍'] An emoji that will replace the spinner when it finishes. - * @param {Number} [options.indentLevel=1] The indent level. - * @param {Number} [options.total] If specified, the spinner contains a counter. It starts from `0`. To increase its value, + * @param {string} title Description of the current processed task. + * @param {object} [options={}] + * @param {boolean} [options.isDisabled] Whether the spinner should be disabled. + * @param {string} [options.emoji='📍'] An emoji that will replace the spinner when it finishes. + * @param {number} [options.indentLevel=1] The indent level. + * @param {number} [options.total] If specified, the spinner contains a counter. It starts from `0`. To increase its value, * call the `#increase()` method on the returned instance of the spinner. - * @param {String|CKEditor5SpinnerStatus} [options.status='[title] Status: [current]/[total].'] If a spinner is a counter, + * @param {string|CKEditor5SpinnerStatus} [options.status='[title] Status: [current]/[total].'] If a spinner is a counter, * this option allows customizing the displayed line. * @returns {CKEditor5Spinner} */ -module.exports = function createSpinner( title, options = {} ) { +export default function createSpinner( title, options = {} ) { const isEnabled = !options.isDisabled && isInteractive(); const indentLevel = options.indentLevel || 0; const indent = ' '.repeat( indentLevel * INDENT_SIZE ); @@ -113,10 +111,10 @@ module.exports = function createSpinner( title, options = {} ) { readline.clearLine( process.stdout, 1 ); readline.cursorTo( process.stdout, 0 ); } -}; +} /** - * @typedef {Object} CKEditor5Spinner + * @typedef {object} CKEditor5Spinner * * @property {CKEditor5SpinnerStart} start * @@ -136,17 +134,17 @@ module.exports = function createSpinner( title, options = {} ) { /** * @callback CKEditor5SpinnerFinish * - * @param {Object} [options={}] + * @param {object} [options={}] * - * @param {String} [options.emoji] + * @param {string} [options.emoji] */ /** * @callback CKEditor5SpinnerStatus * - * @param {String} title + * @param {string} title * - * @param {Number} current + * @param {number} current * - * @param {Number} total + * @param {number} total */ diff --git a/packages/ckeditor5-dev-utils/lib/tools/getdirectories.js b/packages/ckeditor5-dev-utils/lib/tools/getdirectories.js new file mode 100644 index 000000000..e863c0b57 --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/tools/getdirectories.js @@ -0,0 +1,27 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import fs from 'fs'; +import path from 'path'; + +/** + * Returns array with all directories under specified path. + * + * @param {string} directoryPath + * @returns {Array} + */ +export default function getDirectories( directoryPath ) { + const isDirectory = path => { + try { + return fs.statSync( path ).isDirectory(); + } catch ( e ) { + return false; + } + }; + + return fs.readdirSync( directoryPath ).filter( item => { + return isDirectory( path.join( directoryPath, item ) ); + } ); +} diff --git a/packages/ckeditor5-dev-utils/lib/tools/index.js b/packages/ckeditor5-dev-utils/lib/tools/index.js new file mode 100644 index 000000000..311ade2d7 --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/tools/index.js @@ -0,0 +1,9 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +export { default as createSpinner } from './createspinner.js'; +export { default as getDirectories } from './getdirectories.js'; +export { default as shExec } from './shexec.js'; +export { default as updateJSONFile } from './updatejsonfile.js'; diff --git a/packages/ckeditor5-dev-utils/lib/tools/shexec.js b/packages/ckeditor5-dev-utils/lib/tools/shexec.js new file mode 100644 index 000000000..8b84c221b --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/tools/shexec.js @@ -0,0 +1,85 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import chalk from 'chalk'; +import sh from 'shelljs'; +import logger from '../logger/index.js'; + +/** + * Executes a shell command. + * + * @param {string} command The command to be executed. + * @param {object} options + * @param {'info'|'warning'|'error'|'silent'} [options.verbosity='info'] Level of the verbosity. If set as 'info' + * both outputs (stdout and stderr) will be logged. If set as 'error', only stderr output will be logged. + * @param {string} [options.cwd=process.cwd()] + * @param {boolean} [options.async=false] If set, the command execution is asynchronous. The execution is synchronous by default. + * @returns {string|Promise.} The command output. + */ +export default function shExec( command, options = {} ) { + const { + verbosity = 'info', + cwd = process.cwd(), + async = false + } = options; + + sh.config.silent = true; + + const execOptions = { cwd }; + + if ( async ) { + return new Promise( ( resolve, reject ) => { + sh.exec( command, execOptions, ( code, stdout, stderr ) => { + try { + const result = execHandler( { code, stdout, stderr, verbosity, command } ); + + resolve( result ); + } catch ( err ) { + reject( err ); + } + } ); + } ); + } + + const { code, stdout, stderr } = sh.exec( command, execOptions ); + + return execHandler( { code, stdout, stderr, verbosity, command } ); +} + +/** + * @param {object} options + * @param {number} options.code + * @param {string} options.stdout + * @param {string} options.stderr + * @param {'info'|'warning'|'error'|'silent'} options.verbosity + * @param {string} options.command + * @returns {string} + */ +function execHandler( { code, stdout, stderr, verbosity, command } ) { + const log = logger( verbosity ); + const grey = chalk.grey; + + if ( code ) { + if ( stdout ) { + log.error( grey( stdout ) ); + } + + if ( stderr ) { + log.error( grey( stderr ) ); + } + + throw new Error( `Error while executing ${ command }: ${ stderr }` ); + } + + if ( stdout ) { + log.info( grey( stdout ) ); + } + + if ( stderr ) { + log.info( grey( stderr ) ); + } + + return stdout; +} diff --git a/packages/ckeditor5-dev-utils/lib/tools/updatejsonfile.js b/packages/ckeditor5-dev-utils/lib/tools/updatejsonfile.js new file mode 100644 index 000000000..f8a98203c --- /dev/null +++ b/packages/ckeditor5-dev-utils/lib/tools/updatejsonfile.js @@ -0,0 +1,21 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import fs from 'fs'; + +/** + * Updates JSON file under specified path. + * + * @param {string} filePath Path to file on disk. + * @param {function} updateFunction Function that will be called with parsed JSON object. It should return + * modified JSON object to save. + */ +export default function updateJSONFile( filePath, updateFunction ) { + const contents = fs.readFileSync( filePath, 'utf-8' ); + let json = JSON.parse( contents ); + json = updateFunction( json ); + + fs.writeFileSync( filePath, JSON.stringify( json, null, 2 ) + '\n', 'utf-8' ); +} diff --git a/packages/ckeditor5-dev-utils/package.json b/packages/ckeditor5-dev-utils/package.json index 8a8ee239c..a77bb5cc6 100644 --- a/packages/ckeditor5-dev-utils/package.json +++ b/packages/ckeditor5-dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-dev-utils", - "version": "43.0.0", + "version": "44.0.0-alpha.5", "description": "Utils for CKEditor 5 development tools packages.", "keywords": [], "author": "CKSource (http://cksource.com/)", @@ -17,43 +17,41 @@ "npm": ">=5.7.1" }, "main": "lib/index.js", + "type": "module", "files": [ "lib" ], "dependencies": { - "@ckeditor/ckeditor5-dev-translations": "^43.0.0", - "chalk": "^3.0.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.6.1", - "css-loader": "^5.2.7", - "cssnano": "^6.0.3", - "del": "^5.0.0", - "esbuild-loader": "~3.0.1", - "fs-extra": "^11.2.0", - "is-interactive": "^1.0.0", - "javascript-stringify": "^1.6.0", + "@ckeditor/ckeditor5-dev-translations": "^44.0.0-alpha.5", + "chalk": "^5.0.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.0.0", + "css-loader": "^7.0.0", + "cssnano": "^7.0.0", + "esbuild-loader": "^4.0.0", + "fs-extra": "^11.0.0", + "is-interactive": "^2.0.0", "mini-css-extract-plugin": "^2.4.2", - "mocha": "^7.1.2", + "mocha": "^10.0.0", "postcss": "^8.4.12", - "postcss-import": "^14.1.0", - "postcss-loader": "^4.3.0", - "postcss-mixins": "^9.0.2", - "postcss-nesting": "^10.1.4", + "postcss-import": "^16.0.0", + "postcss-loader": "^8.0.0", + "postcss-mixins": "^11.0.0", + "postcss-nesting": "^13.0.0", "raw-loader": "^4.0.1", "shelljs": "^0.8.1", - "style-loader": "^2.0.0", - "terser-webpack-plugin": "^4.2.3", - "through2": "^3.0.1" + "style-loader": "^4.0.0", + "terser-webpack-plugin": "^5.0.0", + "through2": "^4.0.0" }, "devDependencies": { - "chai": "^4.2.0", - "mockery": "^2.1.0", - "sinon": "^9.2.4", - "vinyl": "^2.1.0" + "jest-extended": "^4.0.2", + "vinyl": "^3.0.0", + "vitest": "^2.0.5" }, "scripts": { - "test": "mocha './tests/**/*.js' --timeout 10000", - "coverage": "nyc --reporter=lcov --reporter=text-summary yarn run test" + "test": "vitest run --config vitest.config.js", + "coverage": "vitest run --config vitest.config.js --coverage" }, "depcheckIgnore": [ "css-loader", diff --git a/packages/ckeditor5-dev-utils/tests/_utils/testsetup.js b/packages/ckeditor5-dev-utils/tests/_utils/testsetup.js new file mode 100644 index 000000000..52e540b96 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/_utils/testsetup.js @@ -0,0 +1,9 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { expect } from 'vitest'; +import * as matchers from 'jest-extended'; + +expect.extend( matchers ); diff --git a/packages/ckeditor5-dev-utils/tests/builds/getdllpluginwebpackconfig.js b/packages/ckeditor5-dev-utils/tests/builds/getdllpluginwebpackconfig.js index 7408203ba..4187c0f46 100644 --- a/packages/ckeditor5-dev-utils/tests/builds/getdllpluginwebpackconfig.js +++ b/packages/ckeditor5-dev-utils/tests/builds/getdllpluginwebpackconfig.js @@ -3,19 +3,23 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const path = require( 'path' ); -const chai = require( 'chai' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); -const TerserPlugin = require( 'terser-webpack-plugin' ); -const expect = chai.expect; - -describe( 'builds/getDllPluginWebpackConfig()', () => { - let sandbox, stubs, getDllPluginWebpackConfig; - - const manifest = { +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'fs-extra'; +import getDllPluginWebpackConfig from '../../lib/builds/getdllpluginwebpackconfig.js'; +import { getIconsLoader, getStylesLoader, getTypeScriptLoader } from '../../lib/loaders/index.js'; + +const stubs = vi.hoisted( () => ( { + CKEditorTranslationsPlugin: { + constructor: vi.fn() + }, + TerserPlugin: { + constructor: vi.fn() + }, + webpack: { + BannerPlugin: vi.fn(), + DllReferencePlugin: vi.fn() + }, + manifest: { content: { '../../node_modules/lodash-es/_DataView.js': { id: '../../node_modules/lodash-es/_DataView.js', @@ -27,54 +31,56 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { } } } - }; + } +} ) ); + +vi.mock( '../../lib/loaders/index.js' ); +vi.mock( '../../lib/bundler/index.js' ); +vi.mock( 'fs-extra' ); +vi.mock( 'path', () => ( { + default: { + join: vi.fn( ( ...chunks ) => chunks.join( '/' ) ), + dirname: vi.fn() + } +} ) ); +vi.mock( '@ckeditor/ckeditor5-dev-translations', () => ( { + CKEditorTranslationsPlugin: class CKEditorTranslationsPlugin { + constructor( ...args ) { + this.name = 'CKEditorTranslationsPlugin'; + + stubs.CKEditorTranslationsPlugin.constructor( ...args ); + } + } +} ) ); +vi.mock( 'terser-webpack-plugin', () => ( { + default: class TerserPlugin { + constructor( ...args ) { + stubs.TerserPlugin.constructor( ...args ); + } + } +} ) ); +describe( 'getDllPluginWebpackConfig()', () => { beforeEach( () => { - sandbox = sinon.createSandbox(); - - stubs = { - fs: { - existsSync: sandbox.stub(), - readJsonSync: sandbox.stub() - }, - webpack: { - BannerPlugin: sandbox.stub(), - DllReferencePlugin: sandbox.stub() - }, - loaders: { - getIconsLoader: sinon.stub(), - getStylesLoader: sinon.stub(), - getTypeScriptLoader: sinon.stub() + vi.mocked( fs ).readJsonSync.mockImplementation( input => { + if ( input === '/manifest/path' ) { + return stubs.manifest; } - }; - - stubs.fs.readJsonSync.returns( { - name: '@ckeditor/ckeditor5-dev' - } ); - sandbox.stub( path, 'join' ).callsFake( ( ...args ) => args.join( '/' ) ); + if ( input === '/package/html-embed/package.json' ) { + return { + name: '@ckeditor/ckeditor5-html-embed' + }; + } - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false + return { + name: '@ckeditor/ckeditor5-dev' + }; } ); - - mockery.registerMock( 'fs-extra', stubs.fs ); - mockery.registerMock( '../loaders', stubs.loaders ); - mockery.registerMock( '/manifest/path', manifest ); - - getDllPluginWebpackConfig = require( '../../lib/builds/getdllpluginwebpackconfig' ); } ); - afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); - sandbox.restore(); - } ); - - it( 'returns the webpack configuration in production mode by default', () => { - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { + it( 'returns the webpack configuration in production mode by default', async () => { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' @@ -95,16 +101,12 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { expect( webpackConfig.optimization.minimizer.length ).to.equal( 1 ); // Due to versions mismatch, the `instanceof` check does not pass. - expect( webpackConfig.optimization.minimizer[ 0 ].constructor.name ).to.equal( TerserPlugin.name ); + expect( webpackConfig.optimization.minimizer[ 0 ].constructor.name ).to.equal( 'TerserPlugin' ); } ); - it( 'transforms package with many dashes in its name', () => { - stubs.fs.readJsonSync.returns( { - name: '@ckeditor/ckeditor5-html-embed' - } ); - - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { - packagePath: '/package/path', + it( 'transforms package with many dashes in its name', async () => { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { + packagePath: '/package/html-embed', themePath: '/theme/path', manifestPath: '/manifest/path' } ); @@ -114,8 +116,8 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { expect( webpackConfig.output.filename ).to.equal( 'html-embed.js' ); } ); - it( 'does not minify the destination file when in dev mode', () => { - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { + it( 'does not minify the destination file when in dev mode', async () => { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path', @@ -127,8 +129,8 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { expect( webpackConfig.optimization.minimizer ).to.be.undefined; } ); - it( 'should not export any library by default', () => { - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { + it( 'should not export any library by default', async () => { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' @@ -137,10 +139,10 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { expect( webpackConfig.output.libraryExport ).to.be.undefined; } ); - it( 'uses index.ts entry file by default', () => { - stubs.fs.existsSync.callsFake( file => file == '/package/path/src/index.ts' ); + it( 'uses index.ts entry file by default', async () => { + vi.mocked( fs ).existsSync.mockImplementation( file => file === '/package/path/src/index.ts' ); - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' @@ -149,10 +151,10 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { expect( webpackConfig.entry ).to.equal( '/package/path/src/index.ts' ); } ); - it( 'uses index.js entry file if exists (over its TS version)', () => { - stubs.fs.existsSync.callsFake( file => file == '/package/path/src/index.js' ); + it( 'uses index.js entry file if exists (over its TS version)', async () => { + vi.mocked( fs ).existsSync.mockImplementation( file => file === '/package/path/src/index.js' ); - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' @@ -161,10 +163,10 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { expect( webpackConfig.entry ).to.equal( '/package/path/src/index.js' ); } ); - it( 'loads JavaScript files over TypeScript when building for a JavaScript package', () => { - stubs.fs.existsSync.callsFake( file => file == '/package/path/src/index.js' ); + it( 'loads JavaScript files over TypeScript when building for a JavaScript package', async () => { + vi.mocked( fs ).existsSync.mockImplementation( file => file === '/package/path/src/index.js' ); - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' @@ -174,8 +176,8 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { } ); describe( '#plugins', () => { - it( 'loads the webpack.DllReferencePlugin plugin', () => { - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { + it( 'loads the webpack.DllReferencePlugin plugin', async () => { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' @@ -184,16 +186,17 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { const dllReferencePlugin = webpackConfig.plugins.find( plugin => plugin instanceof stubs.webpack.DllReferencePlugin ); expect( dllReferencePlugin ).to.be.an.instanceOf( stubs.webpack.DllReferencePlugin ); - expect( stubs.webpack.DllReferencePlugin.firstCall.args[ 0 ].manifest ).to.deep.equal( manifest ); - expect( stubs.webpack.DllReferencePlugin.firstCall.args[ 0 ].scope ).to.equal( 'ckeditor5/src' ); - expect( stubs.webpack.DllReferencePlugin.firstCall.args[ 0 ].name ).to.equal( 'CKEditor5.dll' ); - expect( stubs.webpack.DllReferencePlugin.firstCall.args[ 0 ].extensions ).to.be.undefined; + expect( stubs.webpack.DllReferencePlugin ).toHaveBeenCalledExactlyOnceWith( { + manifest: stubs.manifest, + scope: 'ckeditor5/src', + name: 'CKEditor5.dll' + } ); } ); - it( 'loads the CKEditorTranslationsPlugin plugin when lang dir exists', () => { - stubs.fs.existsSync.returns( true ); + it( 'loads the CKEditorTranslationsPlugin plugin when lang dir exists', async () => { + vi.mocked( fs ).existsSync.mockReturnValue( true ); - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' @@ -204,19 +207,26 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { .find( plugin => plugin.constructor.name === 'CKEditorTranslationsPlugin' ); expect( ckeditor5TranslationsPlugin ).to.not.be.undefined; - expect( ckeditor5TranslationsPlugin.options.language ).to.equal( 'en' ); - expect( ckeditor5TranslationsPlugin.options.additionalLanguages ).to.equal( 'all' ); - expect( ckeditor5TranslationsPlugin.options.skipPluralFormFunction ).to.equal( true ); - expect( 'src/bold.js' ).to.match( ckeditor5TranslationsPlugin.options.sourceFilesPattern ); - expect( 'src/bold.ts' ).to.match( ckeditor5TranslationsPlugin.options.sourceFilesPattern ); - expect( 'ckeditor5-basic-styles/src/bold.js' ).to.not.match( ckeditor5TranslationsPlugin.options.sourceFilesPattern ); - expect( 'ckeditor5-basic-styles/src/bold.ts' ).to.not.match( ckeditor5TranslationsPlugin.options.sourceFilesPattern ); + + expect( stubs.CKEditorTranslationsPlugin.constructor ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + language: 'en', + additionalLanguages: 'all', + skipPluralFormFunction: true + } ) ); + + const [ firstCall ] = stubs.CKEditorTranslationsPlugin.constructor.mock.calls; + const { sourceFilesPattern } = firstCall[ 0 ]; + + expect( 'src/bold.js' ).to.match( sourceFilesPattern ); + expect( 'src/bold.ts' ).to.match( sourceFilesPattern ); + expect( 'ckeditor5-basic-styles/src/bold.js' ).to.not.match( sourceFilesPattern ); + expect( 'ckeditor5-basic-styles/src/bold.ts' ).to.not.match( sourceFilesPattern ); } ); - it( 'does not load the CKEditorTranslationsPlugin plugin when lang dir does not exist', () => { - stubs.fs.existsSync.returns( false ); + it( 'does not load the CKEditorTranslationsPlugin plugin when lang dir does not exist', async () => { + vi.mocked( fs ).existsSync.mockReturnValue( false ); - const webpackConfig = getDllPluginWebpackConfig( stubs.webpack, { + const webpackConfig = await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' @@ -232,62 +242,58 @@ describe( 'builds/getDllPluginWebpackConfig()', () => { describe( '#loaders', () => { describe( 'getTypeScriptLoader()', () => { - it( 'it should use the default tsconfig.json if the "options.tsconfigPath" option is not specified', () => { - getDllPluginWebpackConfig( stubs.webpack, { + it( 'it should use the default tsconfig.json if the "options.tsconfigPath" option is not specified', async () => { + await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' } ); - expect( stubs.loaders.getTypeScriptLoader.calledOnce ).to.equal( true ); - - const options = stubs.loaders.getTypeScriptLoader.firstCall.args[ 0 ]; - expect( options ).to.have.property( 'configFile', 'tsconfig.json' ); + expect( vi.mocked( getTypeScriptLoader ) ).toHaveBeenCalledExactlyOnceWith( { + configFile: 'tsconfig.json' + } ); } ); - it( 'it should the specified "options.tsconfigPath" value', () => { - getDllPluginWebpackConfig( stubs.webpack, { + it( 'it should the specified "options.tsconfigPath" value', async () => { + await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path', tsconfigPath: '/config/tsconfig.json' } ); - expect( stubs.loaders.getTypeScriptLoader.calledOnce ).to.equal( true ); - - const options = stubs.loaders.getTypeScriptLoader.firstCall.args[ 0 ]; - expect( options ).to.have.property( 'configFile', '/config/tsconfig.json' ); + expect( vi.mocked( getTypeScriptLoader ) ).toHaveBeenCalledExactlyOnceWith( { + configFile: '/config/tsconfig.json' + } ); } ); } ); describe( 'getIconsLoader()', () => { - it( 'it should get the loader', () => { - getDllPluginWebpackConfig( stubs.webpack, { + it( 'it should get the loader', async () => { + await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' } ); - expect( stubs.loaders.getIconsLoader.calledOnce ).to.equal( true ); - - const options = stubs.loaders.getIconsLoader.firstCall.args[ 0 ]; - expect( options ).to.have.property( 'matchExtensionOnly', true ); + expect( vi.mocked( getIconsLoader ) ).toHaveBeenCalledExactlyOnceWith( { + matchExtensionOnly: true + } ); } ); } ); describe( 'getStylesLoader()', () => { - it( 'it should get the loader', () => { - getDllPluginWebpackConfig( stubs.webpack, { + it( 'it should get the loader', async () => { + await getDllPluginWebpackConfig( stubs.webpack, { packagePath: '/package/path', themePath: '/theme/path', manifestPath: '/manifest/path' } ); - expect( stubs.loaders.getStylesLoader.calledOnce ).to.equal( true ); - - const options = stubs.loaders.getStylesLoader.firstCall.args[ 0 ]; - expect( options ).to.have.property( 'minify', true ); - expect( options ).to.have.property( 'themePath', '/theme/path' ); + expect( vi.mocked( getStylesLoader ) ).toHaveBeenCalledExactlyOnceWith( { + minify: true, + themePath: '/theme/path' + } ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/builds/index.js b/packages/ckeditor5-dev-utils/tests/builds/index.js index 4d644a0ea..1ff005c06 100644 --- a/packages/ckeditor5-dev-utils/tests/builds/index.js +++ b/packages/ckeditor5-dev-utils/tests/builds/index.js @@ -3,21 +3,17 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi } from 'vitest'; +import * as tasks from '../../lib/builds/index.js'; +import getDllPluginWebpackConfig from '../../lib/builds/getdllpluginwebpackconfig.js'; -const chai = require( 'chai' ); -const expect = chai.expect; - -describe( 'builds', () => { - let tasks; - - beforeEach( () => { - tasks = require( '../../lib/builds/index' ); - } ); +vi.mock( '../../lib/builds/getdllpluginwebpackconfig.js' ); +describe( 'builds/index.js', () => { describe( 'getDllPluginWebpackConfig()', () => { it( 'should be a function', () => { expect( tasks.getDllPluginWebpackConfig ).to.be.a( 'function' ); + expect( tasks.getDllPluginWebpackConfig ).toEqual( getDllPluginWebpackConfig ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/bundler/createentryfile.js b/packages/ckeditor5-dev-utils/tests/bundler/createentryfile.js deleted file mode 100644 index 5ca2e12a0..000000000 --- a/packages/ckeditor5-dev-utils/tests/bundler/createentryfile.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const fs = require( 'fs' ); -const chai = require( 'chai' ); -const expect = chai.expect; -const sinon = require( 'sinon' ); - -describe( 'bundler', () => { - let createEntryFile, sandbox; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - createEntryFile = require( '../../lib/bundler/createentryfile' ); - } ); - - afterEach( () => { - sandbox.restore(); - } ); - - describe( 'createEntryFile()', () => { - it( 'should create an entry file', () => { - const writeFileSyncStub = sandbox.stub( fs, 'writeFileSync' ); - - createEntryFile( 'destination/path/file.js', { - plugins: [ - '@ckeditor/ckeditor5-basic-styles/src/bold', - '@ckeditor/ckeditor5-clipboard/src/clipboard' - ], - moduleName: 'ClassicEditor', - editor: '@ckeditor/ckeditor5-editor-classic/src/editor', - config: { - undo: { - step: 3 - }, - toolbar: [ - 'image' - ] - } - } ); - - const expectedEntryFile = `/** - * @license Copyright (c) 2003-${ new Date().getFullYear() }, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/editor'; -import BoldPlugin from '@ckeditor/ckeditor5-basic-styles/src/bold'; -import ClipboardPlugin from '@ckeditor/ckeditor5-clipboard/src/clipboard'; - -export default class ClassicEditor extends ClassicEditorBase {} - -ClassicEditor.build = { - plugins: [ - BoldPlugin, - ClipboardPlugin - ], - config: { - undo: { - step: 3 - }, - toolbar: [ - 'image' - ] - } -}; -`; - - expect( writeFileSyncStub.calledOnce ).to.equal( true ); - expect( writeFileSyncStub.firstCall.args[ 0 ] ).to.equal( 'destination/path/file.js' ); - expect( writeFileSyncStub.firstCall.args[ 1 ] ).to.equal( expectedEntryFile ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-dev-utils/tests/bundler/geteditorconfig.js b/packages/ckeditor5-dev-utils/tests/bundler/geteditorconfig.js deleted file mode 100644 index 7602cac52..000000000 --- a/packages/ckeditor5-dev-utils/tests/bundler/geteditorconfig.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const chai = require( 'chai' ); -const expect = chai.expect; - -describe( 'bundler', () => { - let getEditorConfig; - - beforeEach( () => { - getEditorConfig = require( '../../lib/bundler/geteditorconfig' ); - } ); - - describe( 'getEditorConfig()', () => { - it( 'returns empty object as string if config was not specified', () => { - expect( getEditorConfig() ).to.equal( '{}' ); - } ); - - it( 'returns given object as string with proper indents', () => { - const config = { - firstKey: 1, - secondKey: [ 1, 2, 3 ], - plugin: { - enabled: true, - key: 'PRIVATE_KEY' - }, - 'key with spaces': null, - anotherKey: 'Key with "quotation marks".' - }; - - const expectedConfig = `{ - firstKey: 1, - secondKey: [ - 1, - 2, - 3 - ], - plugin: { - enabled: true, - key: 'PRIVATE_KEY' - }, - 'key with spaces': null, - anotherKey: 'Key with "quotation marks".' - }`; - - expect( getEditorConfig( config ) ).to.equal( expectedConfig ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-dev-utils/tests/bundler/getlicensebanner.js b/packages/ckeditor5-dev-utils/tests/bundler/getlicensebanner.js index 0a3d68dc4..ee8762975 100644 --- a/packages/ckeditor5-dev-utils/tests/bundler/getlicensebanner.js +++ b/packages/ckeditor5-dev-utils/tests/bundler/getlicensebanner.js @@ -3,13 +3,11 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it } from 'vitest'; +import getLicenseBanner from '../../lib/bundler/getlicensebanner.js'; -const expect = require( 'chai' ).expect; -const getLicenseBanner = require( '../../lib/bundler/getlicensebanner' ); - -describe( 'bundler', () => { - describe( 'getLicenseBanner()', () => { +describe( 'getLicenseBanner()', () => { + it( 'should return a banner', () => { expect( getLicenseBanner() ).to.match( /\/\*![\S\s]+\*\//g ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/bundler/getplugins.js b/packages/ckeditor5-dev-utils/tests/bundler/getplugins.js deleted file mode 100644 index 82822f16b..000000000 --- a/packages/ckeditor5-dev-utils/tests/bundler/getplugins.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const chai = require( 'chai' ); -const expect = chai.expect; - -describe( 'bundler', () => { - let getPlugins; - - beforeEach( () => { - getPlugins = require( '../../lib/bundler/getplugins' ); - } ); - - describe( 'getPlugins()', () => { - it( 'returns plugin names and paths', () => { - const plugins = getPlugins( [ - '@ckeditor/ckeditor5-essentials/src/essentials', - '@ckeditor/ckeditor5-basic-styles/src/bold', - '@ckeditor/ckeditor5-basic-styles/src/italic' - ] ); - - expect( plugins ).to.have.property( 'EssentialsPlugin', '@ckeditor/ckeditor5-essentials/src/essentials' ); - expect( plugins ).to.have.property( 'BoldPlugin', '@ckeditor/ckeditor5-basic-styles/src/bold' ); - expect( plugins ).to.have.property( 'ItalicPlugin', '@ckeditor/ckeditor5-basic-styles/src/italic' ); - } ); - - it( 'does not duplicate plugins with the same name', () => { - const plugins = getPlugins( [ - '@ckeditor/ckeditor5-essentials/src/essentials', - 'ckeditor5-foo/src/essentials' - ] ); - - expect( plugins ).to.have.property( 'EssentialsPlugin', '@ckeditor/ckeditor5-essentials/src/essentials' ); - expect( plugins ).to.have.property( 'Essentials1Plugin', 'ckeditor5-foo/src/essentials' ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-dev-utils/tests/bundler/index.js b/packages/ckeditor5-dev-utils/tests/bundler/index.js index 67aa7dada..dc4a89ec4 100644 --- a/packages/ckeditor5-dev-utils/tests/bundler/index.js +++ b/packages/ckeditor5-dev-utils/tests/bundler/index.js @@ -3,27 +3,17 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi } from 'vitest'; +import * as bundler from '../../lib/bundler/index.js'; +import getLicenseBanner from '../../lib/bundler/getlicensebanner.js'; -const chai = require( 'chai' ); -const expect = chai.expect; - -describe( 'bundler', () => { - let tasks; - - beforeEach( () => { - tasks = require( '../../lib/bundler/index' ); - } ); - - describe( 'createEntryFile()', () => { - it( 'should be a function', () => { - expect( tasks.createEntryFile ).to.be.a( 'function' ); - } ); - } ); +vi.mock( '../../lib/bundler/getlicensebanner.js' ); +describe( 'bundler/index.js', () => { describe( 'getLicenseBanner()', () => { it( 'should be a function', () => { - expect( tasks.getLicenseBanner ).to.be.a( 'function' ); + expect( bundler.getLicenseBanner ).to.be.a( 'function' ); + expect( bundler.getLicenseBanner ).toEqual( getLicenseBanner ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/index.js b/packages/ckeditor5-dev-utils/tests/index.js new file mode 100644 index 000000000..f3887f2cd --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/index.js @@ -0,0 +1,66 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import logger from '../lib/logger/index.js'; +import * as packageUtils from '../lib/index.js'; +import * as bundler from '../lib/bundler/index.js'; +import * as loaders from '../lib/loaders/index.js'; +import * as builds from '../lib/builds/index.js'; +import * as stream from '../lib/stream/index.js'; +import * as styles from '../lib/styles/index.js'; +import * as tools from '../lib/tools/index.js'; + +vi.mock( '../lib/builds/index.js' ); +vi.mock( '../lib/bundler/index.js' ); +vi.mock( '../lib/loaders/index.js' ); +vi.mock( '../lib/logger/index.js' ); +vi.mock( '../lib/stream/index.js' ); +vi.mock( '../lib/styles/index.js' ); +vi.mock( '../lib/tools/index.js' ); + +describe( 'index.js', () => { + describe( '#builds', () => { + it( 'should be a function', () => { + expect( packageUtils.builds ).to.equal( builds ); + } ); + } ); + + describe( '#bundler', () => { + it( 'should be a function', () => { + expect( packageUtils.bundler ).to.equal( bundler ); + } ); + } ); + + describe( '#loaders', () => { + it( 'should be a function', () => { + expect( packageUtils.loaders ).to.equal( loaders ); + } ); + } ); + + describe( '#logger', () => { + it( 'should be a function', () => { + expect( packageUtils.logger ).to.equal( logger ); + } ); + } ); + + describe( '#stream', () => { + it( 'should be a function', () => { + expect( packageUtils.stream ).to.equal( stream ); + } ); + } ); + + describe( '#styles', () => { + it( 'should be a function', () => { + expect( packageUtils.styles ).to.equal( styles ); + } ); + } ); + + describe( '#tools', () => { + it( 'should be a function', () => { + expect( packageUtils.tools ).to.equal( tools ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/loaders/getcoverageloader.js b/packages/ckeditor5-dev-utils/tests/loaders/getcoverageloader.js new file mode 100644 index 000000000..a6b4c3be3 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/loaders/getcoverageloader.js @@ -0,0 +1,85 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import path from 'path'; +import { describe, expect, it } from 'vitest'; +import getCoverageLoader from '../../lib/loaders/getcoverageloader.js'; + +const escapedPathSep = path.sep == '/' ? '/' : '\\\\'; + +describe( 'getCoverageLoader()', () => { + it( 'should be a function', () => { + expect( getCoverageLoader ).to.be.a( 'function' ); + } ); + + it( 'should return a definition containing a loader for measuring the coverage', () => { + const coverageLoader = getCoverageLoader( { + files: [] + } ); + + expect( coverageLoader ).to.be.an( 'object' ); + expect( '/path/to/javascript.js' ).to.match( coverageLoader.test ); + expect( '/path/to/typescript.ts' ).to.match( coverageLoader.test ); + + expect( coverageLoader.include ).to.be.an( 'array' ); + expect( coverageLoader.include ).to.lengthOf( 0 ); + expect( coverageLoader.exclude ).to.be.an( 'array' ); + expect( coverageLoader.exclude ).to.lengthOf( 1 ); + + expect( coverageLoader.use ).to.be.an( 'array' ); + expect( coverageLoader.use ).to.lengthOf( 1 ); + + const babelLoader = coverageLoader.use[ 0 ]; + + expect( babelLoader.loader ).to.equal( 'babel-loader' ); + } ); + + it( 'should return a definition containing a loader for measuring the coverage (include glob check)', () => { + const coverageLoader = getCoverageLoader( { + files: [ + // -f utils + [ 'node_modules/ckeditor5-utils/tests/**/*.js' ] + ] + } ); + + expect( coverageLoader ).to.be.an( 'object' ); + expect( coverageLoader ).to.have.property( 'include' ); + expect( coverageLoader.include ).to.be.an( 'array' ); + expect( coverageLoader.include ).to.deep.equal( [ + new RegExp( [ 'ckeditor5-utils', 'src', '' ].join( escapedPathSep ) ) + ] ); + } ); + + it( 'should return a definition containing a loader for measuring the coverage (exclude glob check)', () => { + const coverageLoader = getCoverageLoader( { + files: [ + // -f !utils + [ 'node_modules/ckeditor5-!(utils)/tests/**/*.js' ] + ] + } ); + + expect( coverageLoader ).to.be.an( 'object' ); + expect( coverageLoader ).to.have.property( 'include' ); + expect( coverageLoader.include ).to.be.an( 'array' ); + expect( coverageLoader.include ).to.deep.equal( [ + new RegExp( [ 'ckeditor5-!(utils)', 'src', '' ].join( escapedPathSep ) ) + ] ); + } ); + + it( 'should return a definition containing a loader for measuring the coverage (for root named ckeditor5-*)', () => { + const coverageLoader = getCoverageLoader( { + files: [ + [ '/ckeditor5-collab/packages/ckeditor5-alignment/tests/**/*.{js,ts}' ] + ] + } ); + + expect( coverageLoader ).to.be.an( 'object' ); + expect( coverageLoader ).to.have.property( 'include' ); + expect( coverageLoader.include ).to.be.an( 'array' ); + expect( coverageLoader.include ).to.deep.equal( [ + new RegExp( [ 'ckeditor5-alignment', 'src', '' ].join( escapedPathSep ) ) + ] ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/loaders/getdebugloader.js b/packages/ckeditor5-dev-utils/tests/loaders/getdebugloader.js new file mode 100644 index 000000000..7f4a1bea4 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/loaders/getdebugloader.js @@ -0,0 +1,24 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it } from 'vitest'; +import getDebugLoader from '../../lib/loaders/getdebugloader.js'; + +describe( 'getDebugLoader()', () => { + it( 'should be a function', () => { + expect( getDebugLoader ).to.be.a( 'function' ); + } ); + + it( 'should return a definition containing a loader for measuring the coverage', () => { + const loader = getDebugLoader( [ 'CK_DEBUG_ENGINE' ] ); + + expect( loader ).toEqual( { + loader: expect.stringMatching( /ck-debug-loader\.js$/ ), + options: { + debugFlags: [ 'CK_DEBUG_ENGINE' ] + } + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/loaders/getformattedtextloader.js b/packages/ckeditor5-dev-utils/tests/loaders/getformattedtextloader.js new file mode 100644 index 000000000..2defc69d5 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/loaders/getformattedtextloader.js @@ -0,0 +1,33 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it } from 'vitest'; +import getFormattedTextLoader from '../../lib/loaders/getformattedtextloader.js'; + +describe( 'getFormattedTextLoader()', () => { + it( 'should be a function', () => { + expect( getFormattedTextLoader ).to.be.a( 'function' ); + } ); + + it( 'should return a definition accepting files that store readable content', () => { + const textLoader = getFormattedTextLoader(); + + expect( textLoader ).to.be.an( 'object' ); + expect( textLoader ).to.have.property( 'use' ); + expect( textLoader.use ).to.include( 'raw-loader' ); + expect( textLoader ).to.have.property( 'test' ); + + const loaderRegExp = textLoader.test; + + expect( 'C:\\Program Files\\ckeditor\\italic.html' ).to.match( loaderRegExp, 'HTML: Windows' ); + expect( '/home/ckeditor/italic.html' ).to.match( loaderRegExp, 'HTML: Linux' ); + + expect( 'C:\\Program Files\\ckeditor\\italic.txt' ).to.match( loaderRegExp, 'TXT: Windows' ); + expect( '/home/ckeditor/italic.txt' ).to.match( loaderRegExp, 'TXT: Linux' ); + + expect( 'C:\\Program Files\\ckeditor\\italic.rtf' ).to.match( loaderRegExp, 'RTF: Windows' ); + expect( '/home/ckeditor/italic.rtf' ).to.match( loaderRegExp, 'RTF: Linux' ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/loaders/geticonsloader.js b/packages/ckeditor5-dev-utils/tests/loaders/geticonsloader.js new file mode 100644 index 000000000..157475454 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/loaders/geticonsloader.js @@ -0,0 +1,41 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it } from 'vitest'; +import getIconsLoader from '../../lib/loaders/geticonsloader.js'; + +describe( 'getIconsLoader()', () => { + it( 'should be a function', () => { + expect( getIconsLoader ).to.be.a( 'function' ); + } ); + + it( 'should return a definition loading the svg files properly (a full CKEditor 5 icon path check)', () => { + const svgLoader = getIconsLoader(); + + expect( svgLoader ).to.be.an( 'object' ); + expect( svgLoader ).to.have.property( 'use' ); + expect( svgLoader.use ).to.include( 'raw-loader' ); + expect( svgLoader ).to.have.property( 'test' ); + + const svgRegExp = svgLoader.test; + + expect( 'C:\\Program Files\\ckeditor\\ckeditor5-basic-styles\\theme\\icons\\italic.svg' ).to.match( svgRegExp, 'Windows' ); + expect( '/home/ckeditor/ckeditor5-basic-styles/theme/icons/italic.svg' ).to.match( svgRegExp, 'Linux' ); + } ); + + it( 'should return a definition loading the svg files properly (accept any svg file)', () => { + const svgLoader = getIconsLoader( { matchExtensionOnly: true } ); + + expect( svgLoader ).to.be.an( 'object' ); + expect( svgLoader ).to.have.property( 'use' ); + expect( svgLoader.use ).to.include( 'raw-loader' ); + expect( svgLoader ).to.have.property( 'test' ); + + const svgRegExp = svgLoader.test; + + expect( 'C:\\Program Files\\ckeditor\\italic.svg' ).to.match( svgRegExp, 'Windows' ); + expect( '/home/ckeditor/italic.svg' ).to.match( svgRegExp, 'Linux' ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/loaders/getjavascriptloader.js b/packages/ckeditor5-dev-utils/tests/loaders/getjavascriptloader.js new file mode 100644 index 000000000..f63c4cad5 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/loaders/getjavascriptloader.js @@ -0,0 +1,43 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import getJavaScriptLoader from '../../lib/loaders/getjavascriptloader.js'; +import getDebugLoader from '../../lib/loaders/getdebugloader.js'; + +vi.mock( '../../lib/loaders/getdebugloader.js' ); + +describe( 'getJavaScriptLoader()', () => { + it( 'should be a function', () => { + expect( getJavaScriptLoader ).to.be.a( 'function' ); + } ); + + it( 'should return a definition that enables the ck-debug-loader', () => { + vi.mocked( getDebugLoader ).mockReturnValue( { + loader: 'ck-debug-loader', + options: { + debug: true + } + } ); + + const debugLoader = getJavaScriptLoader( { + debugFlags: [ 'ENGINE' ] + } ); + + expect( vi.mocked( getDebugLoader ) ).toHaveBeenCalledExactlyOnceWith( [ 'ENGINE' ] ); + + expect( debugLoader ).to.be.an( 'object' ); + expect( debugLoader ).to.have.property( 'test' ); + + expect( 'C:\\Program Files\\ckeditor\\plugin.js' ).to.match( debugLoader.test, 'Windows' ); + expect( '/home/ckeditor/plugin.js' ).to.match( debugLoader.test, 'Linux' ); + + expect( debugLoader ).to.have.property( 'loader' ); + expect( debugLoader.loader ).to.equal( 'ck-debug-loader' ); + expect( debugLoader ).to.have.property( 'options' ); + expect( debugLoader.options ).to.be.an( 'object' ); + expect( debugLoader.options ).to.have.property( 'debug', true ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/loaders/getstylesloader.js b/packages/ckeditor5-dev-utils/tests/loaders/getstylesloader.js new file mode 100644 index 000000000..d0f51d0e2 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/loaders/getstylesloader.js @@ -0,0 +1,99 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import getStylesLoader from '../../lib/loaders/getstylesloader.js'; +import { getPostCssConfig } from '../../lib/styles/index.js'; + +vi.mock( 'mini-css-extract-plugin', () => ( { + default: class { + static get loader() { + return '/path/to/mini-css-extract-plugin/loader'; + } + } +} ) ); +vi.mock( '../../lib/styles/index.js' ); + +describe( 'getStylesLoader()', () => { + it( 'should be a function', () => { + expect( getStylesLoader ).to.be.a( 'function' ); + } ); + + it( 'should return a definition that allow saving the produced CSS into a file using `mini-css-extract-plugin#loader`', () => { + const loader = getStylesLoader( { + extractToSeparateFile: true, + themePath: 'path/to/theme' + } ); + + expect( loader ).to.be.an( 'object' ); + + const cssLoader = loader.use.at( 0 ); + + expect( cssLoader ).to.be.equal( '/path/to/mini-css-extract-plugin/loader' ); + } ); + + it( 'should return a definition that allow attaching the produced CSS on a site using `style-loader`', () => { + const loader = getStylesLoader( { + themePath: 'path/to/theme' + } ); + + expect( loader ).to.be.an( 'object' ); + + const styleLoader = loader.use[ 0 ]; + + expect( styleLoader ).to.be.an( 'object' ); + expect( styleLoader ).to.have.property( 'loader', 'style-loader' ); + expect( styleLoader ).to.have.property( 'options' ); + expect( styleLoader.options ).to.be.an( 'object' ); + expect( styleLoader.options ).to.have.property( 'injectType', 'singletonStyleTag' ); + expect( styleLoader.options ).to.have.property( 'attributes' ); + } ); + + it( 'should return a definition containing the correct setup of the `postcss-loader`', () => { + vi.mocked( getPostCssConfig ).mockReturnValue( 'styles.getPostCssConfig()' ); + + const loader = getStylesLoader( { + themePath: 'path/to/theme' + } ); + + expect( loader ).to.be.an( 'object' ); + + const postCssLoader = loader.use.at( -1 ); + + expect( postCssLoader ).to.be.an( 'object' ); + expect( postCssLoader ).to.have.property( 'loader', 'postcss-loader' ); + expect( postCssLoader ).to.have.property( 'options' ); + expect( postCssLoader.options ).to.be.an( 'object' ); + expect( postCssLoader.options ).to.have.property( 'postcssOptions', 'styles.getPostCssConfig()' ); + + expect( vi.mocked( getPostCssConfig ) ).toHaveBeenCalledExactlyOnceWith( { + minify: false, + sourceMap: false, + themeImporter: { + themePath: 'path/to/theme' + } + } ); + } ); + + it( 'should return a definition containing the correct setup of the `css-loader`', () => { + const loader = getStylesLoader( { + skipPostCssLoader: true + } ); + + for ( const definition of loader.use ) { + expect( definition.loader ).to.not.equal( 'postcss-loader' ); + } + } ); + + it( 'should allow skipping adding the postcss-loader', () => { + const loader = getStylesLoader( { + skipPostCssLoader: true + } ); + + const cssLoader = loader.use.at( -1 ); + + expect( cssLoader ).to.be.equal( 'css-loader' ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/loaders/gettypescriptloader.js b/packages/ckeditor5-dev-utils/tests/loaders/gettypescriptloader.js new file mode 100644 index 000000000..002ba2e42 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/loaders/gettypescriptloader.js @@ -0,0 +1,96 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import getTypeScriptLoader from '../../lib/loaders/gettypescriptloader.js'; +import getDebugLoader from '../../lib/loaders/getdebugloader.js'; + +vi.mock( '../../lib/loaders/getdebugloader.js' ); + +describe( 'getTypeScriptLoader()', () => { + it( 'should be a function', () => { + expect( getTypeScriptLoader ).to.be.a( 'function' ); + } ); + + it( 'should return a definition that allows processing `*.ts` files using esbuild-loader', () => { + const tsLoader = getTypeScriptLoader( { + configFile: '/home/project/configs/tsconfig.json' + } ); + + expect( tsLoader ).to.be.an( 'object' ); + expect( tsLoader ).to.have.property( 'test' ); + + expect( 'C:\\Program Files\\ckeditor\\plugin.ts' ).to.match( tsLoader.test, 'Windows' ); + expect( '/home/ckeditor/plugin.ts' ).to.match( tsLoader.test, 'Linux' ); + + const esbuildLoader = tsLoader.use.find( item => item.loader === 'esbuild-loader' ); + + expect( esbuildLoader ).to.be.an( 'object' ); + expect( esbuildLoader ).to.have.property( 'options' ); + expect( esbuildLoader.options ).to.have.property( 'tsconfig', '/home/project/configs/tsconfig.json' ); + } ); + + it( 'should return a definition that allows processing `*.ts` files using esbuild-loader (skipping `options.configFile`)', () => { + const tsLoader = getTypeScriptLoader(); + + expect( tsLoader ).to.be.an( 'object' ); + expect( tsLoader ).to.have.property( 'test' ); + + expect( 'C:\\Program Files\\ckeditor\\plugin.ts' ).to.match( tsLoader.test, 'Windows' ); + expect( '/home/ckeditor/plugin.ts' ).to.match( tsLoader.test, 'Linux' ); + + const esbuildLoader = tsLoader.use.find( item => item.loader === 'esbuild-loader' ); + + expect( esbuildLoader ).to.be.an( 'object' ); + expect( esbuildLoader ).to.have.property( 'options' ); + expect( esbuildLoader.options ).to.have.property( 'tsconfig', 'tsconfig.json' ); + } ); + + it( 'should return a definition that enables the debug loader before the typescript files', () => { + vi.mocked( getDebugLoader ).mockReturnValue( { + loader: 'ck-debug-loader' + } ); + + const tsLoader = getTypeScriptLoader( { + configFile: '/home/project/configs/tsconfig.json', + includeDebugLoader: true, + debugFlags: [ 'ENGINE' ] + } ); + + const ckDebugLoaderIndex = tsLoader.use.findIndex( item => item.loader.endsWith( 'ck-debug-loader' ) ); + const tsLoaderIndex = tsLoader.use.findIndex( item => item.loader === 'esbuild-loader' ); + + // Webpack reads the "use" array from back to the front. + expect( ckDebugLoaderIndex ).to.equal( 1 ); + expect( tsLoaderIndex ).to.equal( 0 ); + } ); + + it( 'should pass the debug options into the debug loader', () => { + vi.mocked( getDebugLoader ).mockReturnValue( { + loader: 'ck-debug-loader', + options: { + debug: true + } + } ); + + const tsLoader = getTypeScriptLoader( { + configFile: '/home/project/configs/tsconfig.json', + includeDebugLoader: true, + debugFlags: [ 'ENGINE' ] + } ); + + const debugLoader = tsLoader.use.find( item => item.loader.endsWith( 'ck-debug-loader' ) ); + + expect( vi.mocked( getDebugLoader ) ).toHaveBeenCalledExactlyOnceWith( [ 'ENGINE' ] ); + + expect( debugLoader ).to.be.an( 'object' ); + + expect( debugLoader ).to.have.property( 'loader' ); + expect( debugLoader.loader ).to.equal( 'ck-debug-loader' ); + expect( debugLoader ).to.have.property( 'options' ); + expect( debugLoader.options ).to.be.an( 'object' ); + expect( debugLoader.options ).to.have.property( 'debug', true ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/loaders/index.js b/packages/ckeditor5-dev-utils/tests/loaders/index.js index a33a619d0..f6d1e55b3 100644 --- a/packages/ckeditor5-dev-utils/tests/loaders/index.js +++ b/packages/ckeditor5-dev-utils/tests/loaders/index.js @@ -3,357 +3,71 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const { expect } = require( 'chai' ); -const mockery = require( 'mockery' ); -const sinon = require( 'sinon' ); - -const escapedPathSep = require( 'path' ).sep === '/' ? '/' : '\\\\'; - -describe( 'loaders', () => { - let loaders, postCssOptions; - - beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( 'mini-css-extract-plugin', { - loader: '/path/to/mini-css-extract-plugin/loader' - } ); - - mockery.registerMock( '../styles', { - getPostCssConfig: options => { - postCssOptions = options; - - return 'styles.getPostCssConfig()'; - } +import { describe, expect, it, vi } from 'vitest'; +import * as loaders from '../../lib/loaders/index.js'; +import getCoverageLoader from '../../lib/loaders/getcoverageloader.js'; +import getTypeScriptLoader from '../../lib/loaders/gettypescriptloader.js'; +import getDebugLoader from '../../lib/loaders/getdebugloader.js'; +import getIconsLoader from '../../lib/loaders/geticonsloader.js'; +import getFormattedTextLoader from '../../lib/loaders/getformattedtextloader.js'; +import getJavaScriptLoader from '../../lib/loaders/getjavascriptloader.js'; +import getStylesLoader from '../../lib/loaders/getstylesloader.js'; + +vi.mock( '../../lib/loaders/getcoverageloader.js' ); +vi.mock( '../../lib/loaders/gettypescriptloader.js' ); +vi.mock( '../../lib/loaders/getdebugloader.js' ); +vi.mock( '../../lib/loaders/geticonsloader.js' ); +vi.mock( '../../lib/loaders/getformattedtextloader.js' ); +vi.mock( '../../lib/loaders/getjavascriptloader.js' ); +vi.mock( '../../lib/loaders/getstylesloader.js' ); + +describe( 'loaders/index.js', () => { + describe( 'getCoverageLoader()', () => { + it( 'should be a function', () => { + expect( loaders.getCoverageLoader ).to.be.a( 'function' ); + expect( loaders.getCoverageLoader ).toEqual( getCoverageLoader ); } ); - - loaders = require( '../../lib/loaders/index' ); - } ); - - afterEach( () => { - sinon.restore(); - mockery.disable(); - mockery.deregisterAll(); } ); describe( 'getTypeScriptLoader()', () => { it( 'should be a function', () => { expect( loaders.getTypeScriptLoader ).to.be.a( 'function' ); - } ); - - it( 'should return a definition that allows processing `*.ts` files using esbuild-loader', () => { - const tsLoader = loaders.getTypeScriptLoader( { - configFile: '/home/project/configs/tsconfig.json' - } ); - - expect( tsLoader ).to.be.an( 'object' ); - expect( tsLoader ).to.have.property( 'test' ); - - expect( 'C:\\Program Files\\ckeditor\\plugin.ts' ).to.match( tsLoader.test, 'Windows' ); - expect( '/home/ckeditor/plugin.ts' ).to.match( tsLoader.test, 'Linux' ); - - const esbuildLoader = tsLoader.use.find( item => item.loader === 'esbuild-loader' ); - - expect( esbuildLoader ).to.be.an( 'object' ); - expect( esbuildLoader ).to.have.property( 'options' ); - expect( esbuildLoader.options ).to.have.property( 'tsconfig', '/home/project/configs/tsconfig.json' ); - } ); - - it( 'should return a definition that allows processing `*.ts` files using esbuild-loader (skipping `options.configFile`)', () => { - const tsLoader = loaders.getTypeScriptLoader(); - - expect( tsLoader ).to.be.an( 'object' ); - expect( tsLoader ).to.have.property( 'test' ); - - expect( 'C:\\Program Files\\ckeditor\\plugin.ts' ).to.match( tsLoader.test, 'Windows' ); - expect( '/home/ckeditor/plugin.ts' ).to.match( tsLoader.test, 'Linux' ); - - const esbuildLoader = tsLoader.use.find( item => item.loader === 'esbuild-loader' ); - - expect( esbuildLoader ).to.be.an( 'object' ); - expect( esbuildLoader ).to.have.property( 'options' ); - expect( esbuildLoader.options ).to.have.property( 'tsconfig', 'tsconfig.json' ); - } ); - - it( 'should return a definition that enables the debug loader before the typescript files', () => { - const tsLoader = loaders.getTypeScriptLoader( { - configFile: '/home/project/configs/tsconfig.json', - includeDebugLoader: true, - debugFlags: [ 'ENGINE' ] - } ); - - const ckDebugLoaderIndex = tsLoader.use.findIndex( item => item.loader.endsWith( 'ck-debug-loader' ) ); - const tsLoaderIndex = tsLoader.use.findIndex( item => item.loader === 'esbuild-loader' ); - - // Webpack reads the "use" array from back to the front. - expect( ckDebugLoaderIndex ).to.equal( 1 ); - expect( tsLoaderIndex ).to.equal( 0 ); - } ); - - it( 'should pass the debug options into the debug loader', () => { - const tsLoader = loaders.getTypeScriptLoader( { - configFile: '/home/project/configs/tsconfig.json', - includeDebugLoader: true, - debugFlags: [ 'ENGINE' ] - } ); - - const debugLoader = tsLoader.use.find( item => item.loader.endsWith( 'ck-debug-loader' ) ); - - expect( debugLoader ).to.be.an( 'object' ); - expect( debugLoader ).to.have.property( 'loader' ); - expect( debugLoader ).to.have.property( 'options' ); - expect( debugLoader.options ).to.be.an( 'object' ); - expect( debugLoader.options ).to.have.property( 'debugFlags' ); - expect( debugLoader.options.debugFlags ).to.be.an( 'array' ); - expect( debugLoader.options.debugFlags ).to.include( 'ENGINE' ); + expect( loaders.getTypeScriptLoader ).toEqual( getTypeScriptLoader ); } ); } ); - describe( 'getJavaScriptLoader()', () => { + describe( 'getDebugLoader()', () => { it( 'should be a function', () => { - expect( loaders.getJavaScriptLoader ).to.be.a( 'function' ); - } ); - - it( 'should return a definition that enables the ck-debug-loader', () => { - const debugLoader = loaders.getJavaScriptLoader( { - debugFlags: [ 'ENGINE' ] - } ); - - expect( debugLoader ).to.be.an( 'object' ); - expect( debugLoader ).to.have.property( 'test' ); - - expect( 'C:\\Program Files\\ckeditor\\plugin.js' ).to.match( debugLoader.test, 'Windows' ); - expect( '/home/ckeditor/plugin.js' ).to.match( debugLoader.test, 'Linux' ); - - expect( debugLoader ).to.have.property( 'loader' ); - expect( debugLoader.loader.endsWith( 'ck-debug-loader' ) ).to.equal( true ); - expect( debugLoader ).to.have.property( 'options' ); - expect( debugLoader.options ).to.be.an( 'object' ); - expect( debugLoader.options ).to.have.property( 'debugFlags' ); - expect( debugLoader.options.debugFlags ).to.be.an( 'array' ); - expect( debugLoader.options.debugFlags ).to.include( 'ENGINE' ); - } ); - } ); - - describe( 'getStylesLoader()', () => { - it( 'should be a function', () => { - expect( loaders.getStylesLoader ).to.be.a( 'function' ); - } ); - - it( 'should return a definition that allow saving the produced CSS into a file using `mini-css-extract-plugin#loader`', () => { - const loader = loaders.getStylesLoader( { - extractToSeparateFile: true, - themePath: 'path/to/theme' - } ); - - expect( loader ).to.be.an( 'object' ); - - const cssLoader = loader.use[ 0 ]; - - expect( cssLoader ).to.be.equal( '/path/to/mini-css-extract-plugin/loader' ); - } ); - - it( 'should return a definition that allow attaching the produced CSS on a site using `style-loader`', () => { - const loader = loaders.getStylesLoader( { - themePath: 'path/to/theme' - } ); - - expect( loader ).to.be.an( 'object' ); - - const styleLoader = loader.use[ 0 ]; - - expect( styleLoader ).to.be.an( 'object' ); - expect( styleLoader ).to.have.property( 'loader', 'style-loader' ); - expect( styleLoader ).to.have.property( 'options' ); - expect( styleLoader.options ).to.be.an( 'object' ); - expect( styleLoader.options ).to.have.property( 'injectType', 'singletonStyleTag' ); - expect( styleLoader.options ).to.have.property( 'attributes' ); - } ); - - it( 'should return a definition containing the correct setup of the `postcss-loader`', () => { - const loader = loaders.getStylesLoader( { - themePath: 'path/to/theme' - } ); - - expect( loader ).to.be.an( 'object' ); - - // Array.at() is available since Node 16.6. - const postCssLoader = loader.use.pop(); - - expect( postCssLoader ).to.be.an( 'object' ); - expect( postCssLoader ).to.have.property( 'loader', 'postcss-loader' ); - expect( postCssLoader ).to.have.property( 'options' ); - expect( postCssLoader.options ).to.be.an( 'object' ); - expect( postCssLoader.options ).to.have.property( 'postcssOptions', 'styles.getPostCssConfig()' ); - - expect( postCssOptions ).to.be.an( 'object' ); - expect( postCssOptions ).to.have.property( 'themeImporter' ); - expect( postCssOptions ).to.have.property( 'minify', false ); - expect( postCssOptions ).to.have.property( 'sourceMap', false ); - expect( postCssOptions.themeImporter ).to.be.an( 'object' ); - expect( postCssOptions.themeImporter ).to.have.property( 'themePath', 'path/to/theme' ); - } ); - - it( 'should return a definition containing the correct setup of the `css-loader`', () => { - const loader = loaders.getStylesLoader( { - skipPostCssLoader: true - } ); - - for ( const definition of loader.use ) { - expect( definition.loader ).to.not.equal( 'postcss-loader' ); - } - } ); - - it( 'should allow skipping adding the postcss-loader', () => { - const loader = loaders.getStylesLoader( { - skipPostCssLoader: true - } ); - - // Array.at() is available since Node 16.6. - const cssLoader = loader.use.pop(); - - expect( cssLoader ).to.be.equal( 'css-loader' ); + expect( loaders.getDebugLoader ).to.be.a( 'function' ); + expect( loaders.getDebugLoader ).toEqual( getDebugLoader ); } ); } ); describe( 'getIconsLoader()', () => { it( 'should be a function', () => { expect( loaders.getIconsLoader ).to.be.a( 'function' ); - } ); - - it( 'should return a definition loading the svg files properly (a full CKEditor 5 icon path check)', () => { - const svgLoader = loaders.getIconsLoader(); - - expect( svgLoader ).to.be.an( 'object' ); - expect( svgLoader ).to.have.property( 'use' ); - expect( svgLoader.use ).to.include( 'raw-loader' ); - expect( svgLoader ).to.have.property( 'test' ); - - const svgRegExp = svgLoader.test; - - expect( 'C:\\Program Files\\ckeditor\\ckeditor5-basic-styles\\theme\\icons\\italic.svg' ).to.match( svgRegExp, 'Windows' ); - expect( '/home/ckeditor/ckeditor5-basic-styles/theme/icons/italic.svg' ).to.match( svgRegExp, 'Linux' ); - } ); - - it( 'should return a definition loading the svg files properly (accept any svg file)', () => { - const svgLoader = loaders.getIconsLoader( { matchExtensionOnly: true } ); - - expect( svgLoader ).to.be.an( 'object' ); - expect( svgLoader ).to.have.property( 'use' ); - expect( svgLoader.use ).to.include( 'raw-loader' ); - expect( svgLoader ).to.have.property( 'test' ); - - const svgRegExp = svgLoader.test; - - expect( 'C:\\Program Files\\ckeditor\\italic.svg' ).to.match( svgRegExp, 'Windows' ); - expect( '/home/ckeditor/italic.svg' ).to.match( svgRegExp, 'Linux' ); + expect( loaders.getIconsLoader ).toEqual( getIconsLoader ); } ); } ); describe( 'getFormattedTextLoader()', () => { it( 'should be a function', () => { expect( loaders.getFormattedTextLoader ).to.be.a( 'function' ); - } ); - - it( 'should return a definition accepting files that store readable content', () => { - const textLoader = loaders.getFormattedTextLoader(); - - expect( textLoader ).to.be.an( 'object' ); - expect( textLoader ).to.have.property( 'use' ); - expect( textLoader.use ).to.include( 'raw-loader' ); - expect( textLoader ).to.have.property( 'test' ); - - const loaderRegExp = textLoader.test; - - expect( 'C:\\Program Files\\ckeditor\\italic.html' ).to.match( loaderRegExp, 'HTML: Windows' ); - expect( '/home/ckeditor/italic.html' ).to.match( loaderRegExp, 'HTML: Linux' ); - - expect( 'C:\\Program Files\\ckeditor\\italic.txt' ).to.match( loaderRegExp, 'TXT: Windows' ); - expect( '/home/ckeditor/italic.txt' ).to.match( loaderRegExp, 'TXT: Linux' ); - - expect( 'C:\\Program Files\\ckeditor\\italic.rtf' ).to.match( loaderRegExp, 'RTF: Windows' ); - expect( '/home/ckeditor/italic.rtf' ).to.match( loaderRegExp, 'RTF: Linux' ); + expect( loaders.getFormattedTextLoader ).toEqual( getFormattedTextLoader ); } ); } ); - describe( 'getCoverageLoader()', () => { + describe( 'getJavaScriptLoader()', () => { it( 'should be a function', () => { - expect( loaders.getCoverageLoader ).to.be.a( 'function' ); - } ); - - it( 'should return a definition containing a loader for measuring the coverage', () => { - const coverageLoader = loaders.getCoverageLoader( { - files: [] - } ); - - expect( coverageLoader ).to.be.an( 'object' ); - expect( '/path/to/javascript.js' ).to.match( coverageLoader.test ); - expect( '/path/to/typescript.ts' ).to.match( coverageLoader.test ); - - expect( coverageLoader.include ).to.be.an( 'array' ); - expect( coverageLoader.include ).to.lengthOf( 0 ); - expect( coverageLoader.exclude ).to.be.an( 'array' ); - expect( coverageLoader.exclude ).to.lengthOf( 1 ); - - expect( coverageLoader.use ).to.be.an( 'array' ); - expect( coverageLoader.use ).to.lengthOf( 1 ); - - const babelLoader = coverageLoader.use[ 0 ]; - - expect( babelLoader.loader ).to.equal( 'babel-loader' ); - } ); - - it( 'should return a definition containing a loader for measuring the coverage (include glob check)', () => { - const coverageLoader = loaders.getCoverageLoader( { - files: [ - // -f utils - [ 'node_modules/ckeditor5-utils/tests/**/*.js' ] - ] - } ); - - expect( coverageLoader ).to.be.an( 'object' ); - expect( coverageLoader ).to.have.property( 'include' ); - expect( coverageLoader.include ).to.be.an( 'array' ); - expect( coverageLoader.include ).to.deep.equal( [ - new RegExp( [ 'ckeditor5-utils', 'src', '' ].join( escapedPathSep ) ) - ] ); - } ); - - it( 'should return a definition containing a loader for measuring the coverage (exclude glob check)', () => { - const coverageLoader = loaders.getCoverageLoader( { - files: [ - // -f !utils - [ 'node_modules/ckeditor5-!(utils)/tests/**/*.js' ] - ] - } ); - - expect( coverageLoader ).to.be.an( 'object' ); - expect( coverageLoader ).to.have.property( 'include' ); - expect( coverageLoader.include ).to.be.an( 'array' ); - expect( coverageLoader.include ).to.deep.equal( [ - new RegExp( [ 'ckeditor5-!(utils)', 'src', '' ].join( escapedPathSep ) ) - ] ); + expect( loaders.getJavaScriptLoader ).to.be.a( 'function' ); + expect( loaders.getJavaScriptLoader ).toEqual( getJavaScriptLoader ); } ); + } ); - it( 'should return a definition containing a loader for measuring the coverage (for root named ckeditor5-*)', () => { - const coverageLoader = loaders.getCoverageLoader( { - files: [ - [ '/ckeditor5-collab/packages/ckeditor5-alignment/tests/**/*.{js,ts}' ] - ] - } ); - - expect( coverageLoader ).to.be.an( 'object' ); - expect( coverageLoader ).to.have.property( 'include' ); - expect( coverageLoader.include ).to.be.an( 'array' ); - expect( coverageLoader.include ).to.deep.equal( [ - new RegExp( [ 'ckeditor5-alignment', 'src', '' ].join( escapedPathSep ) ) - ] ); + describe( 'getStylesLoader()', () => { + it( 'should be a function', () => { + expect( loaders.getStylesLoader ).to.be.a( 'function' ); + expect( loaders.getStylesLoader ).toEqual( getStylesLoader ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/logger.js b/packages/ckeditor5-dev-utils/tests/logger.js deleted file mode 100644 index b057b25b5..000000000 --- a/packages/ckeditor5-dev-utils/tests/logger.js +++ /dev/null @@ -1,201 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const sinon = require( 'sinon' ); -const chai = require( 'chai' ); -const expect = chai.expect; -const logger = require( '../lib/logger' ); - -describe( 'logger', () => { - const logMessage = 'An example.'; - let sandbox, log; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - } ); - - afterEach( () => { - sandbox.restore(); - } ); - - it( 'provides an API for set verbosity level', () => { - expect( logger ).to.be.a( 'function' ); - } ); - - describe( 'verbosity = info', () => { - beforeEach( () => { - log = logger( 'info' ); - } ); - - describe( 'logger.info()', () => { - it( 'should log a message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.info( logMessage ); - - expect( consoleLog.calledOnce ).to.equal( true ); - expect( consoleLog.firstCall.args[ 0 ] ).to.equal( logMessage ); - - consoleLog.restore(); - } ); - } ); - - describe( 'logger.warning()', () => { - it( 'should log a message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.warning( logMessage ); - - expect( consoleLog.calledOnce ).to.equal( true ); - expect( consoleLog.firstCall.args[ 0 ] ).to.match( new RegExp( logMessage ) ); - - consoleLog.restore(); - } ); - } ); - - describe( 'logger.error()', () => { - it( 'should log a message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.error( logMessage ); - - expect( consoleLog.calledOnce ).to.equal( true ); - expect( consoleLog.firstCall.args[ 0 ] ).to.match( new RegExp( logMessage ) ); - - consoleLog.restore(); - } ); - } ); - } ); - - describe( 'verbosity = warning', () => { - beforeEach( () => { - log = logger( 'warning' ); - } ); - - describe( 'logger.info()', () => { - it( 'should not log any message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.info( logMessage ); - - expect( consoleLog.called ).to.equal( false ); - - consoleLog.restore(); - } ); - } ); - - describe( 'logger.warning()', () => { - it( 'should log a message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.warning( logMessage ); - - expect( consoleLog.calledOnce ).to.equal( true ); - expect( consoleLog.firstCall.args[ 0 ] ).to.match( new RegExp( logMessage ) ); - - consoleLog.restore(); - } ); - } ); - - describe( 'logger.error()', () => { - it( 'should log a message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.error( logMessage ); - - expect( consoleLog.calledOnce ).to.equal( true ); - expect( consoleLog.firstCall.args[ 0 ] ).to.match( new RegExp( logMessage ) ); - - consoleLog.restore(); - } ); - } ); - } ); - - describe( 'verbosity = error', () => { - beforeEach( () => { - log = logger( 'error' ); - } ); - - describe( 'logger.info()', () => { - it( 'should not log any message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.info( logMessage ); - - expect( consoleLog.called ).to.equal( false ); - - consoleLog.restore(); - } ); - } ); - - describe( 'logger.warning()', () => { - it( 'should not log any message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.warning( logMessage ); - - expect( consoleLog.called ).to.equal( false ); - - consoleLog.restore(); - } ); - } ); - - describe( 'logger.error()', () => { - it( 'should log a message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.error( logMessage ); - - expect( consoleLog.calledOnce ).to.equal( true ); - expect( consoleLog.firstCall.args[ 0 ] ).to.match( new RegExp( logMessage ) ); - - consoleLog.restore(); - } ); - } ); - } ); - - describe( 'uses default verbosity', () => { - beforeEach( () => { - log = logger(); - } ); - - describe( 'logger.info()', () => { - it( 'should log a message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - - log.info( logMessage ); - - expect( consoleLog.calledOnce ).to.equal( true ); - expect( consoleLog.firstCall.args[ 0 ] ).to.equal( logMessage ); - - consoleLog.restore(); - } ); - } ); - } ); - - describe( 'printing error', () => { - beforeEach( () => { - log = logger(); - } ); - - it( 'should log a message', () => { - const consoleLog = sandbox.stub( console, 'log' ); - const consoleDir = sandbox.stub( console, 'dir' ); - - const error = new Error(); - - log.error( logMessage, error ); - - expect( consoleDir.calledOnce ).to.equal( true ); - expect( consoleDir.firstCall.args[ 0 ] ).to.equal( error ); - expect( consoleDir.firstCall.args[ 1 ] ).to.deep.equal( { depth: null } ); - - consoleLog.restore(); - consoleDir.restore(); - } ); - } ); -} ); diff --git a/packages/ckeditor5-dev-utils/tests/logger/index.js b/packages/ckeditor5-dev-utils/tests/logger/index.js new file mode 100644 index 000000000..2ecdf99d4 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/logger/index.js @@ -0,0 +1,155 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import logger from '../../lib/logger/index.js'; + +vi.stubGlobal( 'console', { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + dir: vi.fn() +} ); + +describe( 'logger()', () => { + const logMessage = 'An example.'; + let log; + + it( 'provides an API for set verbosity level', () => { + expect( logger ).to.be.a( 'function' ); + } ); + + describe( 'verbosity = info', () => { + beforeEach( () => { + log = logger( 'info' ); + } ); + + describe( 'logger.info()', () => { + it( 'should log a message', () => { + log.info( logMessage ); + + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( + expect.stringMatching( logMessage ) + ); + } ); + } ); + + describe( 'logger.warning()', () => { + it( 'should log a message', () => { + log.warning( logMessage ); + + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( + expect.stringMatching( logMessage ) + ); + } ); + } ); + + describe( 'logger.error()', () => { + it( 'should log a message', () => { + log.error( logMessage ); + + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( + expect.stringMatching( logMessage ) + ); + } ); + } ); + } ); + + describe( 'verbosity = warning', () => { + beforeEach( () => { + log = logger( 'warning' ); + } ); + + describe( 'logger.info()', () => { + it( 'should not log any message', () => { + log.info( logMessage ); + + expect( vi.mocked( console ).log ).not.toHaveBeenCalled(); + } ); + } ); + + describe( 'logger.warning()', () => { + it( 'should log a message', () => { + log.warning( logMessage ); + + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( + expect.stringMatching( logMessage ) + ); + } ); + } ); + + describe( 'logger.error()', () => { + it( 'should log a message', () => { + log.error( logMessage ); + + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( + expect.stringMatching( logMessage ) + ); + } ); + } ); + } ); + + describe( 'verbosity = error', () => { + beforeEach( () => { + log = logger( 'error' ); + } ); + + describe( 'logger.info()', () => { + it( 'should not log any message', () => { + log.info( logMessage ); + + expect( vi.mocked( console ).log ).not.toHaveBeenCalled(); + } ); + } ); + + describe( 'logger.warning()', () => { + it( 'should not log any message', () => { + log.warning( logMessage ); + + expect( vi.mocked( console ).log ).not.toHaveBeenCalled(); + } ); + } ); + + describe( 'logger.error()', () => { + it( 'should log a message', () => { + log.error( logMessage ); + + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( + expect.stringMatching( logMessage ) + ); + } ); + } ); + } ); + + describe( 'uses default verbosity', () => { + beforeEach( () => { + log = logger(); + } ); + + describe( 'logger.info()', () => { + it( 'should log a message', () => { + log.info( logMessage ); + + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( + expect.stringMatching( logMessage ) + ); + } ); + } ); + } ); + + describe( 'printing error', () => { + beforeEach( () => { + log = logger(); + } ); + + it( 'should log a message', () => { + const error = new Error(); + + log.error( logMessage, error ); + + expect( vi.mocked( console ).dir ).toHaveBeenCalledExactlyOnceWith( error, { depth: null } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/stream.js b/packages/ckeditor5-dev-utils/tests/stream.js deleted file mode 100644 index 8f5ae9a44..000000000 --- a/packages/ckeditor5-dev-utils/tests/stream.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const chai = require( 'chai' ); -const expect = chai.expect; -const sinon = require( 'sinon' ); -const stream = require( 'stream' ); -const Vinyl = require( 'vinyl' ); - -describe( 'stream', () => { - const utils = require( '../lib/stream' ); - let sandbox; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - } ); - - afterEach( () => { - sandbox.restore(); - } ); - - describe( 'noop', () => { - it( 'should return PassTrough stream', () => { - const PassThrough = stream.PassThrough; - const ret = utils.noop(); - expect( ret instanceof PassThrough ).to.equal( true ); - } ); - - it( 'should return a duplex stream when given a callback and call that callback', () => { - const spy = sinon.spy(); - const ret = utils.noop( spy ); - - ret.write( 'foo' ); - - expect( spy.called ).to.equal( true ); - expect( ret.writable ).to.equal( true ); - expect( ret.readable ).to.equal( true ); - } ); - - it( 'should wait until a promise returned by the callback is resolved', ( ) => { - let resolved = false; - let resolve; - - const stream = utils.noop( () => { - return new Promise( r => { - resolve = r; - } ); - } ); - - stream - .pipe( - utils.noop( () => { - expect( resolved ).to.equal( true ); - } ) - ); - - stream.write( 'foo' ); - - resolved = true; - resolve(); - } ); - - it( 'should fail when a returned promise is rejected', done => { - const chunks = []; - const stream = utils.noop( chunk => { - return new Promise( ( resolve, reject ) => { - if ( chunk == 'foo' ) { - reject(); - } else { - resolve(); - } - } ); - } ); - - stream.pipe( utils.noop( chunk => { - chunks.push( chunk ); - } ) ); - - stream.on( 'end', () => { - expect( chunks.join() ).to.equal( 'bar' ); - done(); - } ); - - stream.write( 'foo' ); - stream.write( 'bar' ); - stream.end(); - } ); - } ); - - describe( 'isTestFile', () => { - function test( path, expected ) { - it( `returns ${ expected } for ${ path }`, () => { - const file = new Vinyl( { - cwd: './', - path, - contents: Buffer.from( '' ) - } ); - - expect( utils.isTestFile( file ) ).to.equal( expected ); - } ); - } - - test( 'tests/file.js', true ); - test( 'tests/foo/file.js', true ); - test( 'tests/tests.js', true ); - test( 'tests/_utils-tests/foo.js', true ); - - test( 'foo/file.js', false ); - test( 'foo/tests/file.js', false ); - test( 'tests/_foo/file.js', false ); - } ); -} ); diff --git a/packages/ckeditor5-dev-utils/tests/stream/index.js b/packages/ckeditor5-dev-utils/tests/stream/index.js new file mode 100644 index 000000000..f10b02008 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/stream/index.js @@ -0,0 +1,19 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import * as stream from '../../lib/stream/index.js'; +import noop from '../../lib/stream/noop.js'; + +vi.mock( '../../lib/stream/noop.js' ); + +describe( 'stream/index.js', () => { + describe( 'noop()', () => { + it( 'should be a function', () => { + expect( stream.noop ).to.be.a( 'function' ); + expect( stream.noop ).toEqual( noop ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/stream/noop.js b/packages/ckeditor5-dev-utils/tests/stream/noop.js new file mode 100644 index 000000000..9db901ae8 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/stream/noop.js @@ -0,0 +1,78 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import stream from 'stream'; +import noop from '../../lib/stream/noop.js'; + +describe( 'noop()', () => { + it( 'should return PassTrough stream', () => { + const PassThrough = stream.PassThrough; + const ret = noop(); + expect( ret instanceof PassThrough ).to.equal( true ); + } ); + + it( 'should return a duplex stream when given a callback and call that callback', () => { + const spy = vi.fn(); + const ret = noop( spy ); + + ret.write( 'foo' ); + + expect( spy ).toHaveBeenCalledOnce(); + expect( ret.writable ).to.equal( true ); + expect( ret.readable ).to.equal( true ); + } ); + + it( 'should wait until a promise returned by the callback is resolved', () => { + let resolved = false; + let resolve; + + const stream = noop( () => { + return new Promise( r => { + resolve = r; + } ); + } ); + + stream + .pipe( + noop( () => { + expect( resolved ).to.equal( true ); + } ) + ); + + stream.write( 'foo' ); + + resolved = true; + resolve(); + } ); + + it( 'should fail when a returned promise is rejected', () => { + return new Promise( done => { + const chunks = []; + const stream = noop( chunk => { + return new Promise( ( resolve, reject ) => { + if ( chunk === 'foo' ) { + reject(); + } else { + resolve(); + } + } ); + } ); + + stream.pipe( noop( chunk => { + chunks.push( chunk ); + } ) ); + + stream.on( 'end', () => { + expect( chunks.join() ).to.equal( 'bar' ); + done(); + } ); + + stream.write( 'foo' ); + stream.write( 'bar' ); + stream.end(); + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/styles/getpostcssconfig.js b/packages/ckeditor5-dev-utils/tests/styles/getpostcssconfig.js index 8c6227ad6..7a8d06c0d 100644 --- a/packages/ckeditor5-dev-utils/tests/styles/getpostcssconfig.js +++ b/packages/ckeditor5-dev-utils/tests/styles/getpostcssconfig.js @@ -3,86 +3,73 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import postCssImport from 'postcss-import'; +import postCssMixins from 'postcss-mixins'; +import postCssNesting from 'postcss-nesting'; +import cssnano from 'cssnano'; +import themeLogger from '../../lib/styles/themelogger.js'; +import themeImporter from '../../lib/styles/themeimporter.js'; +import getPostCssConfig from '../../lib/styles/getpostcssconfig.js'; -const chai = require( 'chai' ); -const sinon = require( 'sinon' ); -const mockery = require( 'mockery' ); -const expect = chai.expect; - -describe( 'styles', () => { - let getPostCssConfig, stubs; +vi.mock( 'postcss-import' ); +vi.mock( 'postcss-mixins' ); +vi.mock( 'postcss-nesting' ); +vi.mock( 'cssnano' ); +vi.mock( '../../lib/styles/themelogger.js' ); +vi.mock( '../../lib/styles/themeimporter.js' ); +describe( 'getPostCssConfig()', () => { beforeEach( () => { - stubs = { - './themeimporter': sinon.stub().returns( 'postcss-ckeditor5-theme-importer' ), - 'postcss-import': sinon.stub().returns( 'postcss-import' ), - 'postcss-mixins': sinon.stub().returns( 'postcss-mixins' ), - 'postcss-nesting': sinon.stub().returns( 'postcss-nesting' ), - './themelogger': sinon.stub().returns( 'postcss-ckeditor5-theme-logger' ), - cssnano: sinon.stub().returns( 'cssnano' ) - }; - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - for ( const stub in stubs ) { - mockery.registerMock( stub, stubs[ stub ] ); - } - - getPostCssConfig = require( '../../lib/styles/getpostcssconfig' ); + vi.mocked( themeImporter ).mockReturnValue( 'postcss-ckeditor5-theme-importer' ); + vi.mocked( themeLogger ).mockReturnValue( 'postcss-ckeditor5-theme-logger' ); + vi.mocked( postCssImport ).mockReturnValue( 'postcss-import' ); + vi.mocked( postCssMixins ).mockReturnValue( 'postcss-mixins' ); + vi.mocked( postCssNesting ).mockReturnValue( 'postcss-nesting' ); + vi.mocked( cssnano ).mockReturnValue( 'cssnano' ); } ); - afterEach( () => { - mockery.disable(); + it( 'returns PostCSS plugins', () => { + expect( getPostCssConfig().plugins ).to.have.members( [ + 'postcss-import', + 'postcss-ckeditor5-theme-importer', + 'postcss-mixins', + 'postcss-nesting', + 'postcss-ckeditor5-theme-logger' + ] ); } ); - describe( 'getPostCssConfig()', () => { - it( 'returns PostCSS plugins', () => { - expect( getPostCssConfig().plugins ) - .to.have.members( [ - 'postcss-import', - 'postcss-ckeditor5-theme-importer', - 'postcss-mixins', - 'postcss-nesting', - 'postcss-ckeditor5-theme-logger' - ] ); - } ); - - it( 'passes options to the theme importer', () => { - getPostCssConfig( { - themeImporter: { - themePath: 'abc', - debug: true - } - } ); - - sinon.assert.calledWithExactly( stubs[ './themeimporter' ], { + it( 'passes options to the theme importer', () => { + getPostCssConfig( { + themeImporter: { themePath: 'abc', debug: true - } ); + } } ); - // https://github.com/ckeditor/ckeditor5/issues/11730 - it( 'passes options to postcss-nesting', () => { - getPostCssConfig(); - - sinon.assert.calledWithExactly( stubs[ 'postcss-nesting' ], { - noIsPseudoSelector: true - } ); + expect( vi.mocked( themeImporter ) ).toHaveBeenCalledExactlyOnceWith( { + themePath: 'abc', + debug: true } ); + } ); - it( 'supports #sourceMap option', () => { - expect( getPostCssConfig( { sourceMap: true } ).sourceMap ) - .to.equal( 'inline' ); - } ); + // https://github.com/ckeditor/ckeditor5/issues/11730 + it( 'passes options to postcss-nesting', () => { + getPostCssConfig(); - it( 'supports #minify option', () => { - expect( getPostCssConfig( { minify: true } ).plugins.pop() ) - .to.equal( 'cssnano' ); + expect( vi.mocked( postCssNesting ) ).toHaveBeenCalledExactlyOnceWith( { + noIsPseudoSelector: true, + edition: '2021' } ); } ); + + it( 'supports #sourceMap option', () => { + expect( getPostCssConfig( { sourceMap: true } ).sourceMap ) + .to.equal( 'inline' ); + } ); + + it( 'supports #minify option', () => { + expect( getPostCssConfig( { minify: true } ).plugins.pop() ) + .to.equal( 'cssnano' ); + } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/styles/index.js b/packages/ckeditor5-dev-utils/tests/styles/index.js index 1f4317cac..2fb7a4ca3 100644 --- a/packages/ckeditor5-dev-utils/tests/styles/index.js +++ b/packages/ckeditor5-dev-utils/tests/styles/index.js @@ -3,33 +3,26 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it, vi } from 'vitest'; +import * as styles from '../../lib/styles/index.js'; +import getPostCssConfig from '../../lib/styles/getpostcssconfig.js'; +import themeImporter from '../../lib/styles/themeimporter.js'; -const chai = require( 'chai' ); -const expect = chai.expect; - -describe( 'styles', () => { - let tasks; - - beforeEach( () => { - tasks = require( '../../lib/styles/index' ); - } ); +vi.mock( '../../lib/styles/getpostcssconfig.js' ); +vi.mock( '../../lib/styles/themeimporter.js' ); +describe( 'styles/index.js', () => { describe( 'getPostCssConfig()', () => { it( 'should be a function', () => { - expect( tasks.getPostCssConfig ).to.be.a( 'function' ); + expect( styles.getPostCssConfig ).to.be.a( 'function' ); + expect( styles.getPostCssConfig ).toEqual( getPostCssConfig ); } ); } ); describe( 'themeImporter()', () => { it( 'should be a function', () => { - expect( tasks.themeImporter ).to.be.a( 'function' ); - } ); - } ); - - describe( 'themeLogger()', () => { - it( 'should be a function', () => { - expect( tasks.themeLogger ).to.be.a( 'function' ); + expect( styles.themeImporter ).to.be.a( 'function' ); + expect( styles.themeImporter ).toEqual( themeImporter ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/styles/utils/getpackagename.js b/packages/ckeditor5-dev-utils/tests/styles/utils/getpackagename.js index d2ca5e744..49e06c70f 100644 --- a/packages/ckeditor5-dev-utils/tests/styles/utils/getpackagename.js +++ b/packages/ckeditor5-dev-utils/tests/styles/utils/getpackagename.js @@ -3,80 +3,71 @@ * For licensing, see LICENSE.md. */ -'use strict'; +import { describe, expect, it } from 'vitest'; +import getPackageName from '../../../lib/styles/utils/getpackagename.js'; -const { expect } = require( 'chai' ); +describe( 'getPackageName()', () => { + describe( 'Unix paths', () => { + it( 'returns package name for path which starts with package name (simple check)', () => { + checkPackage( '/work/space/ckeditor5-foo/tests/manual/foo.js', 'ckeditor5-foo' ); + } ); -describe( 'dev-tests/utils', () => { - let getPackageName; + it( 'returns package name for path which starts with package name (workspace directory looks like package name)', () => { + checkPackage( + '/Users/foo/ckeditor5-workspace/ckeditor5/packages/ckeditor5-foo/tests/manual/foo.js', + 'ckeditor5-foo' + ); + } ); - beforeEach( () => { - getPackageName = require( '../../../lib/styles/utils/getpackagename' ); - } ); + it( 'returns package name for path which starts with package name (nested dependencies)', () => { + checkPackage( + '/home/foo/ckeditor5/packages/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5-foo/tests/manual/foo.js', + 'ckeditor5-foo' + ); + } ); - describe( 'getPackageName()', () => { - describe( 'Unix paths', () => { - it( 'returns package name for path which starts with package name (simple check)', () => { - checkPackage( '/work/space/ckeditor5-foo/tests/manual/foo.js', 'ckeditor5-foo' ); - } ); + /* eslint-disable max-len */ + it( 'returns package name for path which starts with package name (combined workspace looks like package and nested dependencies)', () => { + checkPackage( + '/Users/foo/ckeditor5-workspace/ckeditor5/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5-foo/tests/manual/foo.js', + 'ckeditor5-foo' + ); + } ); + /* eslint-enable max-len */ + } ); - it( 'returns package name for path which starts with package name (workspace directory looks like package name)', () => { - checkPackage( - '/Users/foo/ckeditor5-workspace/ckeditor5/packages/ckeditor5-foo/tests/manual/foo.js', - 'ckeditor5-foo' - ); - } ); + describe( 'Windows paths', () => { + it( 'returns package name for path which starts with package name (simple check)', () => { + checkPackage( 'C:\\work\\space\\ckeditor5-foo\\tests\\manual\\foo.js', 'ckeditor5-foo' ); + } ); - it( 'returns package name for path which starts with package name (nested dependencies)', () => { - checkPackage( - '/home/foo/ckeditor5/packages/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5-foo/tests/manual/foo.js', - 'ckeditor5-foo' - ); - } ); + it( 'returns package name for path which starts with package name (workspace directory looks like package name)', () => { + checkPackage( + 'C:\\Document and settings\\foo\\ckeditor5-workspace\\ckeditor5\\packages\\ckeditor5-foo\\tests\\manual\\foo.js', + 'ckeditor5-foo' + ); + } ); + it( 'returns package name for path which starts with package name (nested dependencies)', () => { /* eslint-disable max-len */ - it( 'returns package name for path which starts with package name (combined workspace looks like package and nested dependencies)', () => { - checkPackage( - '/Users/foo/ckeditor5-workspace/ckeditor5/ckeditor5-build-classic/node_modules/@ckeditor/ckeditor5-foo/tests/manual/foo.js', - 'ckeditor5-foo' - ); - } ); + checkPackage( + 'C:\\Document and settings\\ckeditor5\\packages\\ckeditor5-build-classic\\node_modules\\@ckeditor\\ckeditor5-foo\\tests\\manual\\foo.js', + 'ckeditor5-foo' + ); /* eslint-enable max-len */ } ); - describe( 'Windows paths', () => { - it( 'returns package name for path which starts with package name (simple check)', () => { - checkPackage( 'C:\\work\\space\\ckeditor5-foo\\tests\\manual\\foo.js', 'ckeditor5-foo' ); - } ); - - it( 'returns package name for path which starts with package name (workspace directory looks like package name)', () => { - checkPackage( - 'C:\\Document and settings\\foo\\ckeditor5-workspace\\ckeditor5\\packages\\ckeditor5-foo\\tests\\manual\\foo.js', - 'ckeditor5-foo' - ); - } ); - - it( 'returns package name for path which starts with package name (nested dependencies)', () => { - /* eslint-disable max-len */ - checkPackage( - 'C:\\Document and settings\\ckeditor5\\packages\\ckeditor5-build-classic\\node_modules\\@ckeditor\\ckeditor5-foo\\tests\\manual\\foo.js', - 'ckeditor5-foo' - ); - /* eslint-enable max-len */ - } ); - - /* eslint-disable max-len */ - it( 'returns package name for path which starts with package name (combined workspace looks like package and nested dependencies)', () => { - checkPackage( - 'C:\\Users\\foo\\ckeditor5-workspace\\ckeditor5\\ckeditor5-build-classic\\node_modules\\@ckeditor\\ckeditor5-foo\\tests\\manual\\foo.js', - 'ckeditor5-foo' - ); - } ); - /* eslint-enable max-len */ + /* eslint-disable max-len */ + it( 'returns package name for path which starts with package name (combined workspace looks like package and nested dependencies)', () => { + checkPackage( + 'C:\\Users\\foo\\ckeditor5-workspace\\ckeditor5\\ckeditor5-build-classic\\node_modules\\@ckeditor\\ckeditor5-foo\\tests\\manual\\foo.js', + 'ckeditor5-foo' + ); } ); + /* eslint-enable max-len */ } ); - - function checkPackage( filePath, expectedPath ) { - expect( getPackageName( filePath ) ).to.equal( expectedPath ); - } } ); + +function checkPackage( filePath, expectedPath ) { + expect( getPackageName( filePath ) ).to.equal( expectedPath ); +} diff --git a/packages/ckeditor5-dev-utils/tests/tools.js b/packages/ckeditor5-dev-utils/tests/tools.js deleted file mode 100644 index 3c707febc..000000000 --- a/packages/ckeditor5-dev-utils/tests/tools.js +++ /dev/null @@ -1,258 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md. - */ - -'use strict'; - -const chai = require( 'chai' ); -const sinon = require( 'sinon' ); -const expect = chai.expect; -const tools = require( '../lib/tools' ); -const path = require( 'path' ); -const mockery = require( 'mockery' ); - -describe( 'utils', () => { - let sandbox, infoSpy, errorSpy, loggerVerbosity; - - beforeEach( () => { - sandbox = sinon.createSandbox(); - - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - mockery.registerMock( './logger', verbosity => { - loggerVerbosity = verbosity; - infoSpy = sinon.spy(); - errorSpy = sinon.spy(); - - return { - info: infoSpy, - error: errorSpy - }; - } ); - } ); - - afterEach( () => { - sandbox.restore(); - mockery.disable(); - } ); - - describe( 'tools', () => { - describe( 'createSpinner', () => { - it( 'should be defined', () => expect( tools.createSpinner ).to.be.a( 'function' ) ); - } ); - - describe( 'shExec', () => { - it( 'should be defined', () => expect( tools.shExec ).to.be.a( 'function' ) ); - - it( 'should execute command (default cwd)', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0 } ); - const processStub = sandbox.stub( process, 'cwd' ).returns( '/default' ); - - tools.shExec( 'command' ); - - sinon.assert.calledOnce( processStub ); - sinon.assert.calledOnce( execStub ); - expect( execStub.firstCall.args[ 1 ] ).to.be.an( 'object' ); - expect( execStub.firstCall.args[ 1 ] ).to.contain.property( 'cwd', '/default' ); - } ); - - it( 'should execute command with specified cwd', () => { - const cwd = '/home/user'; - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0 } ); - - tools.shExec( 'command', { cwd } ); - - sinon.assert.calledOnce( execStub ); - expect( execStub.firstCall.args[ 1 ] ).to.be.an( 'object' ); - expect( execStub.firstCall.args[ 1 ] ).to.contain.property( 'cwd', cwd ); - } ); - - it( 'should throw error on unsuccessful call', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 1 } ); - - expect( () => { - tools.shExec( 'command' ); - } ).to.throw(); - sinon.assert.calledOnce( execStub ); - } ); - - it( 'should output using log functions when exit code is equal to 0', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0, stdout: 'out', stderr: 'err' } ); - - tools.shExec( 'command' ); - - expect( loggerVerbosity ).to.equal( 'info' ); - expect( execStub.calledOnce ).to.equal( true ); - expect( errorSpy.called ).to.equal( false ); - expect( infoSpy.calledTwice ).to.equal( true ); - expect( infoSpy.firstCall.args[ 0 ] ).to.match( /out/ ); - expect( infoSpy.secondCall.args[ 0 ] ).to.match( /err/ ); - } ); - - it( 'should output using log functions when exit code is not equal to 0', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 1, stdout: 'out', stderr: 'err' } ); - - expect( () => { - tools.shExec( 'command' ); - } ).to.throw(); - - expect( loggerVerbosity ).to.equal( 'info' ); - expect( infoSpy.called ).to.equal( false ); - expect( execStub.calledOnce ).to.equal( true ); - expect( errorSpy.calledTwice ).to.equal( true ); - expect( errorSpy.firstCall.args[ 0 ] ).to.match( /out/ ); - expect( errorSpy.secondCall.args[ 0 ] ).to.match( /err/ ); - } ); - - it( 'should not log if no output from executed command', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).returns( { code: 0, stdout: '', stderr: '' } ); - - tools.shExec( 'command', { verbosity: 'error' } ); - - expect( loggerVerbosity ).to.equal( 'error' ); - expect( execStub.calledOnce ).to.equal( true ); - expect( infoSpy.calledOnce ).to.equal( false ); - expect( errorSpy.calledOnce ).to.equal( false ); - } ); - - it( 'should return a promise when executing a command in asynchronous mode', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).callsFake( ( command, options, callback ) => { - callback( 0 ); - } ); - - return tools.shExec( 'command', { async: true } ) - .then( () => { - sinon.assert.calledOnce( execStub ); - } ); - } ); - - it( 'should throw error on unsuccessful call in asynchronous mode', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).callsFake( ( command, options, callback ) => { - callback( 1 ); - } ); - - return tools.shExec( 'command', { async: true } ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - () => { - sinon.assert.calledOnce( execStub ); - } - ); - } ); - - it( 'should output using log functions when exit code is equal to 0 in asynchronous mode', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).callsFake( ( command, options, callback ) => { - callback( 0, 'out', 'err' ); - } ); - - return tools.shExec( 'command', { async: true } ) - .then( () => { - expect( loggerVerbosity ).to.equal( 'info' ); - expect( execStub.calledOnce ).to.equal( true ); - expect( errorSpy.called ).to.equal( false ); - expect( infoSpy.calledTwice ).to.equal( true ); - expect( infoSpy.firstCall.args[ 0 ] ).to.match( /out/ ); - expect( infoSpy.secondCall.args[ 0 ] ).to.match( /err/ ); - } ); - } ); - - it( 'should output using log functions when exit code is not equal to 0 in asynchronous mode', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).callsFake( ( command, options, callback ) => { - callback( 1, 'out', 'err' ); - } ); - - return tools.shExec( 'command', { async: true } ) - .then( - () => { - throw new Error( 'Expected to be rejected.' ); - }, - () => { - expect( loggerVerbosity ).to.equal( 'info' ); - expect( infoSpy.called ).to.equal( false ); - expect( execStub.calledOnce ).to.equal( true ); - expect( errorSpy.calledTwice ).to.equal( true ); - expect( errorSpy.firstCall.args[ 0 ] ).to.match( /out/ ); - expect( errorSpy.secondCall.args[ 0 ] ).to.match( /err/ ); - } - ); - } ); - - it( 'should not log if no output from executed command in asynchronous mode', () => { - const sh = require( 'shelljs' ); - const execStub = sandbox.stub( sh, 'exec' ).callsFake( ( command, options, callback ) => { - callback( 0, '', '' ); - } ); - - return tools.shExec( 'command', { verbosity: 'error', async: true } ) - .then( () => { - expect( loggerVerbosity ).to.equal( 'error' ); - expect( execStub.calledOnce ).to.equal( true ); - expect( infoSpy.calledOnce ).to.equal( false ); - expect( errorSpy.calledOnce ).to.equal( false ); - } ); - } ); - } ); - - describe( 'getDirectories', () => { - it( 'should be defined', () => expect( tools.getDirectories ).to.be.a( 'function' ) ); - - it( 'should get directories in specified path', () => { - const fs = require( 'fs' ); - const directories = [ 'dir1', 'dir2', 'dir3' ]; - const readdirSyncStub = sandbox.stub( fs, 'readdirSync' ).returns( directories ); - const statSyncStub = sandbox.stub( fs, 'statSync' ).returns( { - isDirectory: () => { - return true; - } - } ); - const dirPath = 'path'; - - tools.getDirectories( dirPath ); - - expect( readdirSyncStub.calledOnce ).to.equal( true ); - expect( statSyncStub.calledThrice ).to.equal( true ); - expect( statSyncStub.firstCall.args[ 0 ] ).to.equal( path.join( dirPath, directories[ 0 ] ) ); - expect( statSyncStub.secondCall.args[ 0 ] ).to.equal( path.join( dirPath, directories[ 1 ] ) ); - expect( statSyncStub.thirdCall.args[ 0 ] ).to.equal( path.join( dirPath, directories[ 2 ] ) ); - } ); - } ); - - describe( 'updateJSONFile', () => { - it( 'should be defined', () => expect( tools.updateJSONFile ).to.be.a( 'function' ) ); - it( 'should read, update and save JSON file', () => { - const path = 'path/to/file.json'; - const fs = require( 'fs' ); - const readFileStub = sandbox.stub( fs, 'readFileSync' ).returns( '{}' ); - const modifiedJSON = { modified: true }; - const writeFileStub = sandbox.stub( fs, 'writeFileSync' ); - - tools.updateJSONFile( path, () => { - return modifiedJSON; - } ); - - expect( readFileStub.calledOnce ).to.equal( true ); - expect( readFileStub.firstCall.args[ 0 ] ).to.equal( path ); - expect( writeFileStub.calledOnce ).to.equal( true ); - expect( writeFileStub.firstCall.args[ 0 ] ).to.equal( path ); - expect( writeFileStub.firstCall.args[ 1 ] ).to.equal( JSON.stringify( modifiedJSON, null, 2 ) + '\n' ); - } ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-dev-utils/tests/tools/createspinner.js b/packages/ckeditor5-dev-utils/tests/tools/createspinner.js index 382a0baf5..fb519392c 100644 --- a/packages/ckeditor5-dev-utils/tests/tools/createspinner.js +++ b/packages/ckeditor5-dev-utils/tests/tools/createspinner.js @@ -3,54 +3,38 @@ * For licensing, see LICENSE.md. */ -'use strict'; - -const sinon = require( 'sinon' ); -const expect = require( 'chai' ).expect; -const mockery = require( 'mockery' ); - -describe( 'lib/utils/create-spinner', () => { - let createSpinner, clock, stubs; +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import cliCursor from 'cli-cursor'; +import isInteractive from 'is-interactive'; +import createSpinner from '../../lib/tools/createspinner.js'; +import readline from 'readline'; + +vi.mock( 'is-interactive' ); +vi.mock( 'cli-spinners', () => ( { + default: { + dots12: { + frames: [ '|', '/', '-', '\\' ], + interval: 5 + } + } +} ) ); +vi.mock( 'cli-cursor' ); +vi.mock( 'readline' ); + +vi.stubGlobal( 'console', { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn() +} ); +describe( 'createSpinner()', () => { beforeEach( () => { - mockery.enable( { - useCleanCache: true, - warnOnReplace: false, - warnOnUnregistered: false - } ); - - clock = sinon.useFakeTimers(); - - stubs = { - isInteractive: sinon.stub(), - cliSpinners: { - dots12: { - frames: [ '|', '/', '-', '\\' ], - interval: 5 - } - }, - cliCursor: { - show: sinon.stub(), - hide: sinon.stub() - }, - readline: { - clearLine: sinon.stub(), - cursorTo: sinon.stub() - } - }; - - mockery.registerMock( 'is-interactive', stubs.isInteractive ); - mockery.registerMock( 'cli-spinners', stubs.cliSpinners ); - mockery.registerMock( 'cli-cursor', stubs.cliCursor ); - mockery.registerMock( 'readline', stubs.readline ); - - createSpinner = require( '../../lib/tools/createspinner' ); + vi.useFakeTimers(); + vi.setSystemTime( new Date( '2023-06-15 12:00:00' ) ); } ); afterEach( () => { - sinon.restore(); - clock.restore(); - mockery.disable(); + vi.useRealTimers(); } ); it( 'should be a function', () => { @@ -75,44 +59,25 @@ describe( 'lib/utils/create-spinner', () => { describe( 'type: spinner', () => { describe( '#start', () => { beforeEach( () => { - stubs.isInteractive.returns( true ); + vi.mocked( isInteractive ).mockReturnValue( true ); } ); it( 'prints the specified title if spinner should be disabled', () => { const spinner = createSpinner( 'Foo.', { isDisabled: true } ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( '📍 Foo.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( '📍 Foo.' ); } ); it( 'prints the specified title if spinner cannot be created if CLI is not interactive', () => { - stubs.isInteractive.returns( false ); - - const spinner = createSpinner( 'Foo.' ); - const consoleStub = sinon.stub( console, 'log' ); - - spinner.start(); - - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( '📍 Foo.' ); - } ); + vi.mocked( isInteractive ).mockReturnValue( false ); - it( 'uses "setInterval" for creating a loop', () => { const spinner = createSpinner( 'Foo.' ); spinner.start(); - const timer = Object.values( clock.timers ).shift(); - - expect( timer ).to.be.an( 'object' ); - expect( timer.type ).to.equal( 'Interval' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( '📍 Foo.' ); } ); it( 'prints always spinner in the last line', () => { @@ -120,39 +85,42 @@ describe( 'lib/utils/create-spinner', () => { spinner.start(); - const writeStub = sinon.stub( process.stdout, 'write' ); + const writeStub = vi.spyOn( process.stdout, 'write' ).mockImplementation( () => {} ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 1 ); - expect( writeStub.getCall( 0 ).args[ 0 ] ).to.equal( '| Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 1 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 2 ); - expect( writeStub.getCall( 1 ).args[ 0 ] ).to.equal( '/ Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 2 ); + expect( writeStub ).toHaveBeenLastCalledWith( '/ Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 3 ); - expect( writeStub.getCall( 2 ).args[ 0 ] ).to.equal( '- Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 3 ); + expect( writeStub ).toHaveBeenLastCalledWith( '- Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 4 ); - expect( writeStub.getCall( 3 ).args[ 0 ] ).to.equal( '\\ Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 4 ); + expect( writeStub ).toHaveBeenLastCalledWith( '\\ Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 5 ); - expect( writeStub.getCall( 4 ).args[ 0 ] ).to.equal( '| Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 5 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| Foo.' ); // It does not clear the last line for an initial spin. - expect( stubs.readline.clearLine.callCount ).to.equal( 4 ); - expect( stubs.readline.cursorTo.callCount ).to.equal( 4 ); + expect( vi.mocked( readline ).clearLine ).toHaveBeenCalledTimes( 4 ); + expect( vi.mocked( readline ).cursorTo ).toHaveBeenCalledTimes( 4 ); - expect( stubs.readline.clearLine.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.clearLine.firstCall.args[ 1 ] ).to.equal( 1 ); + expect( vi.mocked( readline ).clearLine ).toHaveBeenCalledWith( process.stdout, 1 ); + expect( vi.mocked( readline ).cursorTo ).toHaveBeenCalledWith( process.stdout, 0 ); + } ); - expect( stubs.readline.cursorTo.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.cursorTo.firstCall.args[ 1 ] ).to.equal( 0 ); + it( 'uses "setInterval" for creating a loop', () => { + const spinner = createSpinner( 'Foo.', { total: 10 } ); + + spinner.start(); - writeStub.restore(); + expect( vi.getTimerCount() ).toEqual( 1 ); } ); it( 'hides a cursor when starting spinning', () => { @@ -160,7 +128,7 @@ describe( 'lib/utils/create-spinner', () => { spinner.start(); - expect( stubs.cliCursor.hide.calledOnce ).to.equal( true ); + expect( vi.mocked( cliCursor ).hide ).toHaveBeenCalledOnce(); } ); it( 'allows indenting messages by specifying the "options.indentLevel" option', () => { @@ -168,168 +136,130 @@ describe( 'lib/utils/create-spinner', () => { spinner.start(); - const writeStub = sinon.stub( process.stdout, 'write' ); + const writeStub = vi.spyOn( process.stdout, 'write' ).mockImplementation( () => {} ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 1 ); - expect( writeStub.getCall( 0 ).args[ 0 ] ).to.equal( ' | Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 1 ); + expect( writeStub ).toHaveBeenLastCalledWith( ' | Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 2 ); - expect( writeStub.getCall( 1 ).args[ 0 ] ).to.equal( ' / Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 2 ); + expect( writeStub ).toHaveBeenLastCalledWith( ' / Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 3 ); - expect( writeStub.getCall( 2 ).args[ 0 ] ).to.equal( ' - Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 3 ); + expect( writeStub ).toHaveBeenLastCalledWith( ' - Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 4 ); - expect( writeStub.getCall( 3 ).args[ 0 ] ).to.equal( ' \\ Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 4 ); + expect( writeStub ).toHaveBeenLastCalledWith( ' \\ Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 5 ); - expect( writeStub.getCall( 4 ).args[ 0 ] ).to.equal( ' | Foo.' ); - - writeStub.restore(); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 5 ); + expect( writeStub ).toHaveBeenLastCalledWith( ' | Foo.' ); } ); } ); describe( '#finish', () => { beforeEach( () => { - stubs.isInteractive.returns( true ); + vi.mocked( isInteractive ).mockReturnValue( true ); } ); it( 'does nothing if spinner should be disabled', () => { const spinner = createSpinner( 'Foo.', { isDisabled: true } ); - const consoleStub = sinon.stub( console, 'log' ); spinner.finish(); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( false ); + expect( vi.mocked( console ).log ).not.toHaveBeenCalled(); } ); it( 'does nothing if spinner cannot be created if CLI is not interactive', () => { - stubs.isInteractive.returns( false ); + vi.mocked( isInteractive ).mockReturnValue( false ); const spinner = createSpinner( 'Foo.' ); - const consoleStub = sinon.stub( console, 'log' ); spinner.finish(); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( false ); + expect( vi.mocked( console ).log ).not.toHaveBeenCalled(); } ); it( 'clears the interval when finished', () => { const spinner = createSpinner( 'Foo.' ); - const consoleStub = sinon.stub( console, 'log' ); + + expect( vi.getTimerCount() ).toEqual( 0 ); spinner.start(); - const timer = Object.values( clock.timers ).shift(); - expect( timer ).to.be.an( 'object' ); + expect( vi.getTimerCount() ).toEqual( 1 ); spinner.finish(); - const newTimer = Object.values( clock.timers ).shift(); - - expect( timer ).to.be.an( 'object' ); - expect( newTimer ).to.be.undefined; - - consoleStub.restore(); + expect( vi.getTimerCount() ).toEqual( 0 ); } ); it( 'prints the specified title with a pin when finished', () => { const spinner = createSpinner( 'Foo.' ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); spinner.finish(); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( '📍 Foo.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( '📍 Foo.' ); } ); it( 'shows a cursor when finished spinning', () => { const spinner = createSpinner( 'Foo.' ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); spinner.finish(); - consoleStub.restore(); - expect( stubs.cliCursor.show.calledOnce ).to.equal( true ); + expect( vi.mocked( cliCursor ).show ).toHaveBeenCalledOnce(); } ); it( 'allows indenting messages by specifying the "options.indentLevel" option', () => { const spinner = createSpinner( 'Foo.', { indentLevel: 1 } ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); spinner.finish(); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( ' 📍 Foo.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( ' 📍 Foo.' ); } ); it( 'prints the specified emoji when created a spinner if it finished', () => { const spinner = createSpinner( 'Foo.', { emoji: '👉' } ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); spinner.finish(); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( '👉 Foo.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( '👉 Foo.' ); } ); it( 'prints the specified title if spinner cannot be created if CLI is not interactive', () => { - stubs.isInteractive.returns( false ); + vi.mocked( isInteractive ).mockReturnValue( true ); const spinner = createSpinner( 'Foo.', { emoji: '👉' } ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); + spinner.finish(); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( '👉 Foo.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( '👉 Foo.' ); } ); it( 'allows overriding the emoji (use default emoji when creating a spinner)', () => { const spinner = createSpinner( 'Foo.' ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); spinner.finish( { emoji: '❌' } ); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( '❌ Foo.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( '❌ Foo.' ); } ); it( 'allows overriding the emoji (passed an emoji when creating a spinner)', () => { const spinner = createSpinner( 'Foo.', { emoji: '👉' } ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); spinner.finish( { emoji: '❌' } ); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( '❌ Foo.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( '❌ Foo.' ); } ); } ); @@ -339,28 +269,25 @@ describe( 'lib/utils/create-spinner', () => { expect( () => { spinner.increase(); - } ).to.throw( Error, 'The \'#increase()\' method is available only when using the counter spinner.' ); + } ).toThrow( 'The \'#increase()\' method is available only when using the counter spinner.' ); } ); } ); } ); + describe( 'type: counter', () => { beforeEach( () => { - stubs.isInteractive.returns( true ); + vi.mocked( isInteractive ).mockReturnValue( true ); } ); describe( '#start', () => { it( 'prints the specified title if spinner cannot be created if CLI is not interactive', () => { - stubs.isInteractive.returns( false ); + vi.mocked( isInteractive ).mockReturnValue( false ); const spinner = createSpinner( 'Foo.', { total: 10 } ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( '📍 Foo.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( '📍 Foo.' ); } ); it( 'uses "setInterval" for creating a loop', () => { @@ -368,10 +295,7 @@ describe( 'lib/utils/create-spinner', () => { spinner.start(); - const timer = Object.values( clock.timers ).shift(); - - expect( timer ).to.be.an( 'object' ); - expect( timer.type ).to.equal( 'Interval' ); + expect( vi.getTimerCount() ).toEqual( 1 ); } ); it( 'prints always spinner in the last line', () => { @@ -379,39 +303,34 @@ describe( 'lib/utils/create-spinner', () => { spinner.start(); - const writeStub = sinon.stub( process.stdout, 'write' ); + const writeStub = vi.spyOn( process.stdout, 'write' ).mockImplementation( () => {} ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 1 ); - expect( writeStub.getCall( 0 ).args[ 0 ] ).to.equal( '| Foo. Status: 0/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 1 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| Foo. Status: 0/10.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 2 ); - expect( writeStub.getCall( 1 ).args[ 0 ] ).to.equal( '/ Foo. Status: 0/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 2 ); + expect( writeStub ).toHaveBeenLastCalledWith( '/ Foo. Status: 0/10.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 3 ); - expect( writeStub.getCall( 2 ).args[ 0 ] ).to.equal( '- Foo. Status: 0/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 3 ); + expect( writeStub ).toHaveBeenLastCalledWith( '- Foo. Status: 0/10.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 4 ); - expect( writeStub.getCall( 3 ).args[ 0 ] ).to.equal( '\\ Foo. Status: 0/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 4 ); + expect( writeStub ).toHaveBeenLastCalledWith( '\\ Foo. Status: 0/10.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 5 ); - expect( writeStub.getCall( 4 ).args[ 0 ] ).to.equal( '| Foo. Status: 0/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 5 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| Foo. Status: 0/10.' ); // It does not clear the last line for an initial spin. - expect( stubs.readline.clearLine.callCount ).to.equal( 4 ); - expect( stubs.readline.cursorTo.callCount ).to.equal( 4 ); + expect( vi.mocked( readline ).clearLine ).toHaveBeenCalledTimes( 4 ); + expect( vi.mocked( readline ).cursorTo ).toHaveBeenCalledTimes( 4 ); - expect( stubs.readline.clearLine.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.clearLine.firstCall.args[ 1 ] ).to.equal( 1 ); - - expect( stubs.readline.cursorTo.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.cursorTo.firstCall.args[ 1 ] ).to.equal( 0 ); - - writeStub.restore(); + expect( vi.mocked( readline ).clearLine ).toHaveBeenCalledWith( process.stdout, 1 ); + expect( vi.mocked( readline ).cursorTo ).toHaveBeenCalledWith( process.stdout, 0 ); } ); it( 'allows defining a custom progress status (as a string)', () => { @@ -419,39 +338,27 @@ describe( 'lib/utils/create-spinner', () => { spinner.start(); - const writeStub = sinon.stub( process.stdout, 'write' ); - - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 1 ); - expect( writeStub.getCall( 0 ).args[ 0 ] ).to.equal( '| 0 (10) - Foo.' ); + const writeStub = vi.spyOn( process.stdout, 'write' ).mockImplementation( () => {} ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 2 ); - expect( writeStub.getCall( 1 ).args[ 0 ] ).to.equal( '/ 0 (10) - Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 1 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| 0 (10) - Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 3 ); - expect( writeStub.getCall( 2 ).args[ 0 ] ).to.equal( '- 0 (10) - Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 2 ); + expect( writeStub ).toHaveBeenLastCalledWith( '/ 0 (10) - Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 4 ); - expect( writeStub.getCall( 3 ).args[ 0 ] ).to.equal( '\\ 0 (10) - Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 3 ); + expect( writeStub ).toHaveBeenLastCalledWith( '- 0 (10) - Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 5 ); - expect( writeStub.getCall( 4 ).args[ 0 ] ).to.equal( '| 0 (10) - Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 4 ); + expect( writeStub ).toHaveBeenLastCalledWith( '\\ 0 (10) - Foo.' ); - // It does not clear the last line for an initial spin. - expect( stubs.readline.clearLine.callCount ).to.equal( 4 ); - expect( stubs.readline.cursorTo.callCount ).to.equal( 4 ); - - expect( stubs.readline.clearLine.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.clearLine.firstCall.args[ 1 ] ).to.equal( 1 ); - - expect( stubs.readline.cursorTo.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.cursorTo.firstCall.args[ 1 ] ).to.equal( 0 ); - - writeStub.restore(); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 5 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| 0 (10) - Foo.' ); } ); it( 'allows defining a custom progress status (as a callback)', () => { @@ -464,54 +371,38 @@ describe( 'lib/utils/create-spinner', () => { spinner.start(); - const writeStub = sinon.stub( process.stdout, 'write' ); - - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 1 ); - expect( writeStub.getCall( 0 ).args[ 0 ] ).to.equal( '| 0 (10) - Foo.' ); - - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 2 ); - expect( writeStub.getCall( 1 ).args[ 0 ] ).to.equal( '/ 0 (10) - Foo.' ); + const writeStub = vi.spyOn( process.stdout, 'write' ).mockImplementation( () => {} ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 3 ); - expect( writeStub.getCall( 2 ).args[ 0 ] ).to.equal( '- 0 (10) - Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 1 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| 0 (10) - Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 4 ); - expect( writeStub.getCall( 3 ).args[ 0 ] ).to.equal( '\\ 0 (10) - Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 2 ); + expect( writeStub ).toHaveBeenLastCalledWith( '/ 0 (10) - Foo.' ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 5 ); - expect( writeStub.getCall( 4 ).args[ 0 ] ).to.equal( '| 0 (10) - Foo.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 3 ); + expect( writeStub ).toHaveBeenLastCalledWith( '- 0 (10) - Foo.' ); - // It does not clear the last line for an initial spin. - expect( stubs.readline.clearLine.callCount ).to.equal( 4 ); - expect( stubs.readline.cursorTo.callCount ).to.equal( 4 ); - - expect( stubs.readline.clearLine.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.clearLine.firstCall.args[ 1 ] ).to.equal( 1 ); - - expect( stubs.readline.cursorTo.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.cursorTo.firstCall.args[ 1 ] ).to.equal( 0 ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 4 ); + expect( writeStub ).toHaveBeenLastCalledWith( '\\ 0 (10) - Foo.' ); - writeStub.restore(); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 5 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| 0 (10) - Foo.' ); } ); } ); describe( '#finish', () => { it( 'prints the specified title with a pin when finished', () => { const spinner = createSpinner( 'Foo.', { total: 10 } ); - const consoleStub = sinon.stub( console, 'log' ); spinner.start(); spinner.finish(); - consoleStub.restore(); - - expect( consoleStub.calledOnce ).to.equal( true ); - expect( consoleStub.firstCall.args[ 0 ] ).to.equal( '📍 Foo.' ); + expect( vi.mocked( console ).log ).toHaveBeenCalledExactlyOnceWith( '📍 Foo.' ); } ); } ); @@ -521,47 +412,42 @@ describe( 'lib/utils/create-spinner', () => { spinner.start(); - const writeStub = sinon.stub( process.stdout, 'write' ); + const writeStub = vi.spyOn( process.stdout, 'write' ).mockImplementation( () => {} ); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 1 ); - expect( writeStub.getCall( 0 ).args[ 0 ] ).to.equal( '| Foo. Status: 0/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 1 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| Foo. Status: 0/10.' ); spinner.increase(); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 2 ); - expect( writeStub.getCall( 1 ).args[ 0 ] ).to.equal( '/ Foo. Status: 1/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 2 ); + expect( writeStub ).toHaveBeenLastCalledWith( '/ Foo. Status: 1/10.' ); spinner.increase(); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 3 ); - expect( writeStub.getCall( 2 ).args[ 0 ] ).to.equal( '- Foo. Status: 2/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 3 ); + expect( writeStub ).toHaveBeenLastCalledWith( '- Foo. Status: 2/10.' ); spinner.increase(); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 4 ); - expect( writeStub.getCall( 3 ).args[ 0 ] ).to.equal( '\\ Foo. Status: 3/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 4 ); + expect( writeStub ).toHaveBeenLastCalledWith( '\\ Foo. Status: 3/10.' ); spinner.increase(); - clock.tick( 5 ); - expect( writeStub.callCount ).to.equal( 5 ); - expect( writeStub.getCall( 4 ).args[ 0 ] ).to.equal( '| Foo. Status: 4/10.' ); + vi.advanceTimersByTime( 5 ); + expect( writeStub ).toHaveBeenCalledTimes( 5 ); + expect( writeStub ).toHaveBeenLastCalledWith( '| Foo. Status: 4/10.' ); // It does not clear the last line for an initial spin. - expect( stubs.readline.clearLine.callCount ).to.equal( 4 ); - expect( stubs.readline.cursorTo.callCount ).to.equal( 4 ); - - expect( stubs.readline.clearLine.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.clearLine.firstCall.args[ 1 ] ).to.equal( 1 ); - - expect( stubs.readline.cursorTo.firstCall.args[ 0 ] ).to.equal( process.stdout ); - expect( stubs.readline.cursorTo.firstCall.args[ 1 ] ).to.equal( 0 ); + expect( vi.mocked( readline ).clearLine ).toHaveBeenCalledTimes( 4 ); + expect( vi.mocked( readline ).cursorTo ).toHaveBeenCalledTimes( 4 ); - writeStub.restore(); + expect( vi.mocked( readline ).clearLine ).toHaveBeenCalledWith( process.stdout, 1 ); + expect( vi.mocked( readline ).cursorTo ).toHaveBeenCalledWith( process.stdout, 0 ); } ); } ); } ); diff --git a/packages/ckeditor5-dev-utils/tests/tools/getdirectories.js b/packages/ckeditor5-dev-utils/tests/tools/getdirectories.js new file mode 100644 index 000000000..646f92824 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/tools/getdirectories.js @@ -0,0 +1,34 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import fs from 'fs'; +import path from 'path'; +import { describe, expect, it, vi } from 'vitest'; +import getDirectories from '../../lib/tools/getdirectories.js'; + +vi.mock( 'fs' ); + +describe( 'getDirectories()', () => { + it( 'should get directories in specified path', () => { + const directories = [ 'dir1', 'dir2', 'dir3' ]; + + vi.mocked( fs ).readdirSync.mockReturnValue( directories ); + vi.mocked( fs ).statSync.mockReturnValue( { + isDirectory: () => { + return true; + } + } ); + + const dirPath = 'path'; + + getDirectories( dirPath ); + + expect( vi.mocked( fs ).readdirSync ).toHaveBeenCalledExactlyOnceWith( dirPath ); + expect( vi.mocked( fs ).statSync ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( fs ).statSync ).toHaveBeenCalledWith( path.join( dirPath, directories[ 0 ] ) ); + expect( vi.mocked( fs ).statSync ).toHaveBeenCalledWith( path.join( dirPath, directories[ 1 ] ) ); + expect( vi.mocked( fs ).statSync ).toHaveBeenCalledWith( path.join( dirPath, directories[ 2 ] ) ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/tools/index.js b/packages/ckeditor5-dev-utils/tests/tools/index.js new file mode 100644 index 000000000..37cde1c37 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/tools/index.js @@ -0,0 +1,51 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { describe, expect, it, vi } from 'vitest'; +import * as tools from '../../lib/tools/index.js'; +import shExec from '../../lib/tools/shexec.js'; +import createSpinner from '../../lib/tools/createspinner.js'; +import getDirectories from '../../lib/tools/getdirectories.js'; +import updateJSONFile from '../../lib/tools/updatejsonfile.js'; + +vi.mock( '../../lib/tools/shexec.js' ); +vi.mock( '../../lib/tools/createspinner.js' ); +vi.mock( '../../lib/tools/getdirectories.js' ); +vi.mock( '../../lib/tools/updatejsonfile.js' ); + +describe( 'tools/index.js', () => { + describe( 'createSpinner()', () => { + it( 'should be a function', () => { + expect( tools.createSpinner ).to.be.a( 'function' ); + expect( tools.createSpinner ).toEqual( createSpinner ); + } ); + } ); + + describe( 'getDirectories()', () => { + it( 'should be a function', () => { + expect( tools.getDirectories ).to.be.a( 'function' ); + expect( tools.getDirectories ).toEqual( getDirectories ); + } ); + } ); + + describe( 'shExec()', () => { + it( 'should be a function', () => { + expect( tools.shExec ).to.be.a( 'function' ); + expect( tools.shExec ).toEqual( shExec ); + } ); + } ); + + describe( 'updateJSONFile()', () => { + it( 'should be a function', () => { + expect( tools.updateJSONFile ).to.be.a( 'function' ); + expect( tools.updateJSONFile ).toEqual( updateJSONFile ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/tools/shexec.js b/packages/ckeditor5-dev-utils/tests/tools/shexec.js new file mode 100644 index 000000000..6817392e2 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/tools/shexec.js @@ -0,0 +1,171 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import sh from 'shelljs'; +import shExec from '../../lib/tools/shexec.js'; +import logger from '../../lib/logger/index.js'; + +vi.mock( 'shelljs' ); +vi.mock( '../../lib/logger/index.js' ); + +describe( 'shExec()', () => { + let stubs; + + beforeEach( () => { + stubs = { + infoSpy: vi.fn(), + errorSpy: vi.fn() + }; + + vi.spyOn( process, 'cwd' ).mockReturnValue( '/default' ); + + vi.mocked( logger ).mockReturnValue( { + info: stubs.infoSpy, + error: stubs.errorSpy + } ); + } ); + + describe( 'sync', () => { + it( 'should execute a specified command using the default cwd', () => { + vi.mocked( sh ).exec.mockReturnValue( { code: 0 } ); + + shExec( 'command' ); + + expect( vi.mocked( process ).cwd ).toHaveBeenCalledOnce(); + expect( vi.mocked( sh ).exec ).toHaveBeenCalledExactlyOnceWith( + 'command', + expect.objectContaining( { + cwd: '/default' + } ) + ); + } ); + + it( 'should execute command with specified cwd', () => { + const cwd = '/home/user'; + + vi.mocked( sh ).exec.mockReturnValue( { code: 0 } ); + + shExec( 'command', { cwd } ); + + expect( vi.mocked( sh ).exec ).toHaveBeenCalledExactlyOnceWith( + 'command', + expect.objectContaining( { + cwd: '/home/user' + } ) + ); + } ); + + it( 'should throw error on unsuccessful call', () => { + vi.mocked( sh ).exec.mockReturnValue( { code: 1 } ); + + expect( () => { + shExec( 'command' ); + } ).to.throw(); + + expect( vi.mocked( sh ).exec ).toHaveBeenCalledOnce(); + } ); + + it( 'should output using log functions when exit code is equal to 0', () => { + vi.mocked( sh ).exec.mockReturnValue( { code: 0, stdout: 'out', stderr: 'err' } ); + + shExec( 'command' ); + + expect( vi.mocked( sh ).exec ).toHaveBeenCalledOnce(); + expect( vi.mocked( logger ) ).toHaveBeenCalledExactlyOnceWith( 'info' ); + expect( stubs.errorSpy ).not.toHaveBeenCalled(); + expect( stubs.infoSpy ).toHaveBeenCalledTimes( 2 ); + expect( stubs.infoSpy ).toHaveBeenCalledWith( expect.stringContaining( 'out' ) ); + expect( stubs.infoSpy ).toHaveBeenCalledWith( expect.stringContaining( 'err' ) ); + } ); + + it( 'should output using log functions when exit code is not equal to 0', () => { + vi.mocked( sh ).exec.mockReturnValue( { code: 1, stdout: 'out', stderr: 'err' } ); + + expect( () => { + shExec( 'command' ); + } ).to.throw(); + + expect( vi.mocked( sh ).exec ).toHaveBeenCalledOnce(); + expect( vi.mocked( logger ) ).toHaveBeenCalledExactlyOnceWith( 'info' ); + expect( stubs.infoSpy ).not.toHaveBeenCalled(); + expect( stubs.errorSpy ).toHaveBeenCalledTimes( 2 ); + expect( stubs.errorSpy ).toHaveBeenCalledWith( expect.stringContaining( 'out' ) ); + expect( stubs.errorSpy ).toHaveBeenCalledWith( expect.stringContaining( 'err' ) ); + } ); + + it( 'should not log if no output from executed command', () => { + vi.mocked( sh ).exec.mockReturnValue( { code: 0, stdout: '', stderr: '' } ); + + shExec( 'command', { verbosity: 'error' } ); + + expect( vi.mocked( logger ) ).toHaveBeenCalledExactlyOnceWith( 'error' ); + expect( stubs.infoSpy ).not.toHaveBeenCalled(); + expect( stubs.errorSpy ).not.toHaveBeenCalled(); + } ); + } ); + + describe( 'async', () => { + it( 'should return a promise when executing a command in asynchronous mode', async () => { + vi.mocked( sh ).exec.mockImplementation( ( command, options, callback ) => { + callback( 0 ); + } ); + + await shExec( 'command', { async: true } ); + expect( vi.mocked( sh ).exec ).toHaveBeenCalledOnce(); + } ); + + it( 'should throw error on unsuccessful call in asynchronous mode', async () => { + vi.mocked( sh ).exec.mockImplementation( ( command, options, callback ) => { + callback( 1 ); + } ); + + await expect( shExec( 'command', { async: true } ) ).rejects.toThrow(); + expect( vi.mocked( sh ).exec ).toHaveBeenCalledOnce(); + } ); + + it( 'should output using log functions when exit code is equal to 0 in asynchronous mode', async () => { + vi.mocked( sh ).exec.mockImplementation( ( command, options, callback ) => { + callback( 0, 'out', 'err' ); + } ); + + await shExec( 'command', { async: true } ); + + expect( vi.mocked( sh ).exec ).toHaveBeenCalledOnce(); + expect( vi.mocked( logger ) ).toHaveBeenCalledExactlyOnceWith( 'info' ); + expect( stubs.errorSpy ).not.toHaveBeenCalled(); + expect( stubs.infoSpy ).toHaveBeenCalledTimes( 2 ); + expect( stubs.infoSpy ).toHaveBeenCalledWith( expect.stringContaining( 'out' ) ); + expect( stubs.infoSpy ).toHaveBeenCalledWith( expect.stringContaining( 'err' ) ); + } ); + + it( 'should output using log functions when exit code is not equal to 0 in asynchronous mode', async () => { + vi.mocked( sh ).exec.mockImplementation( ( command, options, callback ) => { + callback( 1, 'out', 'err' ); + } ); + + await expect( shExec( 'command', { async: true } ) ).rejects.toThrow(); + + expect( vi.mocked( sh ).exec ).toHaveBeenCalledOnce(); + expect( vi.mocked( logger ) ).toHaveBeenCalledExactlyOnceWith( 'info' ); + expect( stubs.infoSpy ).not.toHaveBeenCalled(); + expect( stubs.errorSpy ).toHaveBeenCalledTimes( 2 ); + expect( stubs.errorSpy ).toHaveBeenCalledWith( expect.stringContaining( 'out' ) ); + expect( stubs.errorSpy ).toHaveBeenCalledWith( expect.stringContaining( 'err' ) ); + } ); + + it( 'should not log if no output from executed command in asynchronous mode', async () => { + vi.mocked( sh ).exec.mockImplementation( ( command, options, callback ) => { + callback( 0, '', '' ); + } ); + + await shExec( 'command', { verbosity: 'error', async: true } ); + + expect( vi.mocked( logger ) ).toHaveBeenCalledExactlyOnceWith( 'error' ); + expect( stubs.infoSpy ).not.toHaveBeenCalled(); + expect( stubs.errorSpy ).not.toHaveBeenCalled(); + } ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/tests/tools/updatejsonfile.js b/packages/ckeditor5-dev-utils/tests/tools/updatejsonfile.js new file mode 100644 index 000000000..a78a7a4f0 --- /dev/null +++ b/packages/ckeditor5-dev-utils/tests/tools/updatejsonfile.js @@ -0,0 +1,30 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import fs from 'fs'; +import { describe, expect, it, vi } from 'vitest'; +import updateJSONFile from '../../lib/tools/updatejsonfile.js'; + +vi.mock( 'fs' ); + +describe( 'updateJSONFile()', () => { + it( 'should read, update and save JSON file', () => { + vi.mocked( fs ).readFileSync.mockReturnValue( '{}' ); + + const path = 'path/to/file.json'; + const modifiedJSON = { modified: true }; + + updateJSONFile( path, () => { + return modifiedJSON; + } ); + + expect( vi.mocked( fs ).readFileSync ).toHaveBeenCalledExactlyOnceWith( path, 'utf-8' ); + expect( vi.mocked( fs ).writeFileSync ).toHaveBeenCalledExactlyOnceWith( + path, + JSON.stringify( modifiedJSON, null, 2 ) + '\n', + 'utf-8' + ); + } ); +} ); diff --git a/packages/ckeditor5-dev-utils/vitest.config.js b/packages/ckeditor5-dev-utils/vitest.config.js new file mode 100644 index 000000000..075897d63 --- /dev/null +++ b/packages/ckeditor5-dev-utils/vitest.config.js @@ -0,0 +1,30 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig( { + test: { + setupFiles: [ + './tests/_utils/testsetup.js' + ], + testTimeout: 10000, + mockReset: true, + restoreMocks: true, + include: [ + 'tests/**/*.js' + ], + exclude: [ + './tests/_utils/**/*.js' + ], + coverage: { + provider: 'v8', + include: [ + 'lib/**' + ], + reporter: [ 'text', 'json', 'html', 'lcov' ] + } + } +} ); diff --git a/packages/ckeditor5-dev-web-crawler/lib/constants.js b/packages/ckeditor5-dev-web-crawler/lib/constants.js index 73e387bf3..6d143ea20 100644 --- a/packages/ckeditor5-dev-web-crawler/lib/constants.js +++ b/packages/ckeditor5-dev-web-crawler/lib/constants.js @@ -7,15 +7,17 @@ /* eslint-env node */ -const DEFAULT_CONCURRENCY = require( 'os' ).cpus().length / 2; +import { cpus } from 'os'; -const DEFAULT_TIMEOUT = 15 * 1000; +export const DEFAULT_CONCURRENCY = cpus().length / 2; -const DEFAULT_RESPONSIVENESS_CHECK_TIMEOUT = 1000; +export const DEFAULT_TIMEOUT = 15 * 1000; -const DEFAULT_REMAINING_ATTEMPTS = 3; +export const DEFAULT_RESPONSIVENESS_CHECK_TIMEOUT = 1000; -const ERROR_TYPES = { +export const DEFAULT_REMAINING_ATTEMPTS = 3; + +export const ERROR_TYPES = { PAGE_CRASH: { event: 'error', description: 'Page crash' @@ -43,7 +45,7 @@ const ERROR_TYPES = { } }; -const PATTERN_TYPE_TO_ERROR_TYPE_MAP = { +export const PATTERN_TYPE_TO_ERROR_TYPE_MAP = { 'page-crash': ERROR_TYPES.PAGE_CRASH, 'uncaught-exception': ERROR_TYPES.UNCAUGHT_EXCEPTION, 'request-failure': ERROR_TYPES.REQUEST_FAILURE, @@ -52,20 +54,8 @@ const PATTERN_TYPE_TO_ERROR_TYPE_MAP = { 'navigation-error': ERROR_TYPES.NAVIGATION_ERROR }; -const IGNORE_ALL_ERRORS_WILDCARD = '*'; - -const META_TAG_NAME = 'x-cke-crawler-ignore-patterns'; +export const IGNORE_ALL_ERRORS_WILDCARD = '*'; -const DATA_ATTRIBUTE_NAME = 'data-cke-crawler-skip'; +export const META_TAG_NAME = 'x-cke-crawler-ignore-patterns'; -module.exports = { - DEFAULT_CONCURRENCY, - DEFAULT_TIMEOUT, - DEFAULT_RESPONSIVENESS_CHECK_TIMEOUT, - DEFAULT_REMAINING_ATTEMPTS, - ERROR_TYPES, - PATTERN_TYPE_TO_ERROR_TYPE_MAP, - IGNORE_ALL_ERRORS_WILDCARD, - META_TAG_NAME, - DATA_ATTRIBUTE_NAME -}; +export const DATA_ATTRIBUTE_NAME = 'data-cke-crawler-skip'; diff --git a/packages/ckeditor5-dev-web-crawler/lib/index.js b/packages/ckeditor5-dev-web-crawler/lib/index.js index e4880d3eb..766b5ad17 100644 --- a/packages/ckeditor5-dev-web-crawler/lib/index.js +++ b/packages/ckeditor5-dev-web-crawler/lib/index.js @@ -7,14 +7,6 @@ /* eslint-env node */ -const runCrawler = require( './runcrawler' ); -const { getBaseUrl, isUrlValid, toArray } = require( './utils' ); -const { DEFAULT_CONCURRENCY } = require( './constants' ); - -module.exports = { - DEFAULT_CONCURRENCY, - runCrawler, - getBaseUrl, - isUrlValid, - toArray -}; +export { default as runCrawler } from './runcrawler.js'; +export { getBaseUrl, isUrlValid, toArray } from './utils.js'; +export { DEFAULT_CONCURRENCY } from './constants.js'; diff --git a/packages/ckeditor5-dev-web-crawler/lib/runcrawler.js b/packages/ckeditor5-dev-web-crawler/lib/runcrawler.js index 2c3e35f99..7146fa277 100644 --- a/packages/ckeditor5-dev-web-crawler/lib/runcrawler.js +++ b/packages/ckeditor5-dev-web-crawler/lib/runcrawler.js @@ -7,14 +7,14 @@ /* eslint-env node */ -const puppeteer = require( 'puppeteer' ); -const chalk = require( 'chalk' ); -const util = require( 'util' ); -const stripAnsiEscapeCodes = require( 'strip-ansi' ); -const { getBaseUrl, toArray } = require( './utils' ); -const { createSpinner, getProgressHandler } = require( './spinner' ); - -const { +import puppeteer from 'puppeteer'; +import chalk from 'chalk'; +import util from 'util'; +import stripAnsiEscapeCodes from 'strip-ansi'; +import { getBaseUrl, toArray } from './utils.js'; +import { createSpinner, getProgressHandler } from './spinner.js'; + +import { DEFAULT_TIMEOUT, DEFAULT_RESPONSIVENESS_CHECK_TIMEOUT, DEFAULT_REMAINING_ATTEMPTS, @@ -23,7 +23,7 @@ const { IGNORE_ALL_ERRORS_WILDCARD, META_TAG_NAME, DATA_ATTRIBUTE_NAME -} = require( './constants' ); +} from './constants.js'; /** * Main crawler function. Its purpose is to: @@ -31,18 +31,18 @@ const { * - open simultaneously (up to concurrency limit) links from the provided URL in a dedicated Puppeteer's page for each link, * - show error summary after all links have been visited. * - * @param {Object} options Parsed CLI arguments. - * @param {String} options.url The URL to start crawling. This argument is required. - * @param {Number} [options.depth=Infinity] Defines how many nested page levels should be examined. Infinity by default. - * @param {Array.} [options.exclusions=[]] An array of patterns to exclude links. Empty array by default to not exclude anything. - * @param {Number} [options.concurrency=1] Number of concurrent pages (browser tabs) to be used during crawling. One by default. - * @param {Boolean} [options.quit=false] Terminates the scan as soon as an error is found. False (off) by default. - * @param {Boolean} [options.disableBrowserSandbox=false] Whether the browser should be created with the `--no-sandbox` flag. - * @param {Boolean} [options.noSpinner=false] Whether to display the spinner with progress or a raw message with current progress. - * @param {Boolean} [options.ignoreHTTPSErrors=false] Whether the browser should ignore invalid (self-signed) certificates. + * @param {object} options Parsed CLI arguments. + * @param {string} options.url The URL to start crawling. This argument is required. + * @param {number} [options.depth=Infinity] Defines how many nested page levels should be examined. Infinity by default. + * @param {Array.} [options.exclusions=[]] An array of patterns to exclude links. Empty array by default to not exclude anything. + * @param {number} [options.concurrency=1] Number of concurrent pages (browser tabs) to be used during crawling. One by default. + * @param {boolean} [options.quit=false] Terminates the scan as soon as an error is found. False (off) by default. + * @param {boolean} [options.disableBrowserSandbox=false] Whether the browser should be created with the `--no-sandbox` flag. + * @param {boolean} [options.noSpinner=false] Whether to display the spinner with progress or a raw message with current progress. + * @param {boolean} [options.ignoreHTTPSErrors=false] Whether the browser should ignore invalid (self-signed) certificates. * @returns {Promise} Promise is resolved, when the crawler has finished the whole crawling procedure. */ -module.exports = async function runCrawler( options ) { +export default async function runCrawler( options ) { const { url, depth = Infinity, @@ -101,16 +101,16 @@ module.exports = async function runCrawler( options ) { // Always exit the script because `spinner` can freeze the process of the crawler if it is executed in the `noSpinner:true` mode. process.exit( errors.size ? 1 : 0 ); -}; +} /** * Creates a new browser instance and closes the default blank page. * - * @param {Object} options - * @param {Boolean} [options.disableBrowserSandbox] Whether the browser should be created with the `--no-sandbox` flag. - * @param {Boolean} [options.ignoreHTTPSErrors] Whether the browser should ignore invalid (self-signed) certificates. + * @param {object} options + * @param {boolean} [options.disableBrowserSandbox] Whether the browser should be created with the `--no-sandbox` flag. + * @param {boolean} [options.ignoreHTTPSErrors] Whether the browser should ignore invalid (self-signed) certificates. * - * @returns {Promise.} A promise, which resolves to the Puppeteer browser instance. + * @returns {Promise.} A promise, which resolves to the Puppeteer browser instance. */ async function createBrowser( options ) { const browserOptions = { @@ -129,10 +129,14 @@ async function createBrowser( options ) { const browser = await puppeteer.launch( browserOptions ); - const [ defaultBlankPage ] = await browser.pages(); + // For unknown reasons, in order to be able to visit pages in Puppeteer on CI, we must close the default page that is opened when the + // browser starts. + if ( process.env.CI ) { + const [ defaultBlankPage ] = await browser.pages(); - if ( defaultBlankPage ) { - await defaultBlankPage.close(); + if ( defaultBlankPage ) { + await defaultBlankPage.close(); + } } return browser; @@ -176,16 +180,16 @@ function getErrorHandler( errors ) { /** * Searches and opens all found links in the document body from requested URL, recursively. * - * @param {Object} browser The headless browser instance from Puppeteer. - * @param {Object} data All data needed for crawling the links. - * @param {String} data.baseUrl The base URL from the initial page URL. + * @param {object} browser The headless browser instance from Puppeteer. + * @param {object} data All data needed for crawling the links. + * @param {string} data.baseUrl The base URL from the initial page URL. * @param {Array.} data.linksQueue An array of link to crawl. - * @param {Array.} data.foundLinks An array of all links, which have been already discovered. - * @param {Array.} data.exclusions An array of patterns to exclude links. Empty array by default to not exclude anything. - * @param {Number} data.concurrency Number of concurrent pages (browser tabs) to be used during crawling. - * @param {Boolean} data.quit Terminates the scan as soon as an error is found. - * @param {Function} data.onError Callback called ever time an error has been found. - * @param {Function} data.onProgress Callback called every time just before opening a new link. + * @param {Array.} data.foundLinks An array of all links, which have been already discovered. + * @param {Array.} data.exclusions An array of patterns to exclude links. Empty array by default to not exclude anything. + * @param {number} data.concurrency Number of concurrent pages (browser tabs) to be used during crawling. + * @param {boolean} data.quit Terminates the scan as soon as an error is found. + * @param {function} data.onError Callback called ever time an error has been found. + * @param {function} data.onProgress Callback called every time just before opening a new link. * @returns {Promise} Promise is resolved, when all links have been visited. */ async function openLinks( browser, { baseUrl, linksQueue, foundLinks, exclusions, concurrency, quit, onError, onProgress } ) { @@ -250,12 +254,12 @@ async function openLinks( browser, { baseUrl, linksQueue, foundLinks, exclusions * excluded links are also skipped. If the requested traversing depth has been reached, nested links from this URL are not collected * anymore. * - * @param {Object} browser The headless browser instance from Puppeteer. - * @param {Object} data All data needed for crawling the link. - * @param {String} data.baseUrl The base URL from the initial page URL. + * @param {object} browser The headless browser instance from Puppeteer. + * @param {object} data All data needed for crawling the link. + * @param {string} data.baseUrl The base URL from the initial page URL. * @param {Link} data.link A link to crawl. - * @param {Array.} data.foundLinks An array of all links, which have been already discovered. - * @param {Array.} data.exclusions An array of patterns to exclude links. Empty array by default to not exclude anything. + * @param {Array.} data.foundLinks An array of all links, which have been already discovered. + * @param {Array.} data.exclusions An array of patterns to exclude links. Empty array by default to not exclude anything. * @returns {Promise.} A promise, which resolves to a collection of unique errors and links. */ async function openLink( browser, { baseUrl, link, foundLinks, exclusions } ) { @@ -326,12 +330,12 @@ async function openLink( browser, { baseUrl, link, foundLinks, exclusions } ) { /** * Finds all links in opened page and filters out external, already discovered and explicitly excluded ones. * - * @param {Object} page The page instance from Puppeteer. - * @param {Object} data All data needed for crawling the link. - * @param {String} data.baseUrl The base URL from the initial page URL. - * @param {Array.} data.foundLinks An array of all links, which have been already discovered. - * @param {Array.} data.exclusions An array patterns to exclude links. Empty array by default to not exclude anything. - * @returns {Promise.>} A promise, which resolves to an array of unique links. + * @param {object} page The page instance from Puppeteer. + * @param {object} data All data needed for crawling the link. + * @param {string} data.baseUrl The base URL from the initial page URL. + * @param {Array.} data.foundLinks An array of all links, which have been already discovered. + * @param {Array.} data.exclusions An array patterns to exclude links. Empty array by default to not exclude anything. + * @returns {Promise.>} A promise, which resolves to an array of unique links. */ async function getLinksFromPage( page, { baseUrl, foundLinks, exclusions } ) { const evaluatePage = anchors => [ ...new Set( anchors @@ -363,8 +367,8 @@ async function getLinksFromPage( page, { baseUrl, foundLinks, exclusions } ) { /** * Finds all meta tags, that contain a pattern to ignore errors, and then returns a map between error type and these patterns. * - * @param {Object} page The page instance from Puppeteer. - * @returns {Promise.>>} A promise, which resolves to a map between an error type and a set of patterns. + * @param {object} page The page instance from Puppeteer. + * @returns {Promise.>>} A promise, which resolves to a map between an error type and a set of patterns. */ async function getErrorIgnorePatternsFromPage( page ) { const metaTag = await page.$( `head > meta[name=${ META_TAG_NAME }]` ); @@ -412,7 +416,7 @@ async function getErrorIgnorePatternsFromPage( page ) { * Iterates over all found errors from given link and marks errors as ignored, if their message match the ignore pattern. * * @param {Array.} errors An array of errors to check. - * @param {Map.>} errorIgnorePatterns A map between an error type and a set of patterns. + * @param {Map.>} errorIgnorePatterns A map between an error type and a set of patterns. */ function markErrorsAsIgnored( errors, errorIgnorePatterns ) { errors.forEach( error => { @@ -449,11 +453,11 @@ function markErrorsAsIgnored( errors, errorIgnorePatterns ) { /** * Creates a new page in Puppeteer's browser instance. * - * @param {Object} browser The headless browser instance from Puppeteer. - * @param {Object} data All data needed for creating a new page. + * @param {object} browser The headless browser instance from Puppeteer. + * @param {object} data All data needed for creating a new page. * @param {Link} data.link A link to crawl. - * @param {Function} data.onError Callback called every time just before opening a new link. - * @returns {Promise.} A promise, which resolves to the page instance from Puppeteer. + * @param {function} data.onError Callback called every time just before opening a new link. + * @returns {Promise.} A promise, which resolves to the page instance from Puppeteer. */ async function createPage( browser, { link, onError } ) { const page = await browser.newPage(); @@ -474,7 +478,7 @@ async function createPage( browser, { link, onError } ) { /** * Dismisses any dialogs (alert, prompt, confirm, beforeunload) that could be displayed on page load. * - * @param {Object} page The page instance from Puppeteer. + * @param {object} page The page instance from Puppeteer. */ function dismissDialogs( page ) { page.on( 'dialog', async dialog => { @@ -485,10 +489,10 @@ function dismissDialogs( page ) { /** * Registers all error handlers on given page instance. * - * @param {Object} page The page instance from Puppeteer. - * @param {Object} data All data needed for registering error handlers. + * @param {object} page The page instance from Puppeteer. + * @param {object} data All data needed for registering error handlers. * @param {Link} data.link A link to crawl associated with Puppeteer's page. - * @param {Function} data.onError Called each time an error has been found. + * @param {function} data.onError Called each time an error has been found. */ function registerErrorHandlers( page, { link, onError } ) { page.on( ERROR_TYPES.PAGE_CRASH.event, error => onError( { @@ -573,9 +577,9 @@ function registerErrorHandlers( page, { link, onError } ) { return argument; }; - const serializeArguments = argument => argument - .executionContext() - .evaluate( serializeArgumentInPageContext, argument ); + const serializeArguments = argument => { + return argument.evaluate( serializeArgumentInPageContext, argument ); + }; const serializedArguments = await Promise.all( message.args().map( serializeArguments ) ); @@ -605,8 +609,8 @@ function registerErrorHandlers( page, { link, onError } ) { * Checks, if HTTP request was a navigation one, i.e. request that is driving frame's navigation. Requests sent from child frames * (i.e. from