diff --git a/.babelrc b/.babelrc deleted file mode 100644 index de74bdcea..000000000 --- a/.babelrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "presets": [["@babel/preset-env", { "loose": true }]], - "env": { - "test": { - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": true - } - } - ] - ] - } - } -} diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index e6fc5a0c7..000000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -types/ -public/ \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index e1e776020..53bfc92a1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,15 +10,17 @@ ], "env": { "es6": true, - "browser": true, - "mocha": true, - "cypress/globals": true + "node": true, + "browser": true }, "parserOptions": { - "project": "./tsconfig.json", - "ecmaVersion": 2020 + "sourceType": "module", + "project": true }, "rules": { + "no-param-reassign": ["error", { "props": false }], + "@typescript-eslint/explicit-function-return-type": "error", + "import/no-named-as-default": "off", "import/prefer-default-export": "off", "import/no-extraneous-dependencies": [ "error", @@ -62,16 +64,27 @@ } ], "lines-between-class-members": "off", + "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-namespace": "off", - "react/jsx-filename-extension": [0] + "react/jsx-filename-extension": [0], + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "never", + "mjs": "never", + "jsx": "never", + "ts": "never", + "tsx": "never" + } + ] }, "overrides": [ { - "files": ["*.test.ts"], - "env": { - "mocha": true - }, + "files": ["*.test.ts", "*.spec.ts"], "rules": { + "no-await-in-loop": "off", + "@typescript-eslint/explicit-function-return-type": "off", "no-restricted-syntax": "off", "compat/compat": "off", "no-new": "off", @@ -88,16 +101,6 @@ } ] } - }, - { - "files": ["cypress/**"], - "plugins": ["cypress"], - "rules": { - "no-unused-vars": "warn" - }, - "env": { - "cypress/globals": true - } } ], "settings": { @@ -112,12 +115,14 @@ "CustomEvent", "Element.prototype.classList", "Element.prototype.closest", - "Element.prototype.dataset" + "Element.prototype.dataset", + "Element.prototype.replaceChildren" ], "import/resolver": { "node": { "extensions": [".js", ".ts"] } } - } + }, + "ignorePatterns": ["node_modules/*", "public/*"] } diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..629ebd2d8 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,15 @@ +# byte shaving (hoist semi-commonly variables, remove some low level low-usage functions) +157a47a44a01e3ce4b54ad211b1756ff59985bef +5bee41d7ff08e05442b232e3e552dcb6c703568d +e9382df0ae63edfc7540f82f74cf969342c759c0 + +# prettier config change +00433d200d8cccc8b544fbc8f05d5e96bf8ccff7 + +# misc linting cleanup +00009d2effa8b41a6ce27ef8b06a35a04215aea6 +62b786d1f13d0934137a62909d3a37db0a3e927e +5ad61841143508c9f91f0edd57f81f8b11066e0a +84a61cad1ddab1e851c98efa619a2cd35af434c1 +33f573247e8badc9ee10defe326f13985342e09b +b0199538a82d49de429f35546e412d14fc8bfeb9 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f3d5c415e..472a3cdaf 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -23,6 +23,9 @@ A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. +**Choices version and bundle** +- Version: [e.g. v11.0.0 choices.min.js] + **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] diff --git a/.github/actions-scripts/__snapshots__/chrome-win32.png b/.github/actions-scripts/__snapshots__/chrome-win32.png deleted file mode 100644 index d85cc5e75..000000000 Binary files a/.github/actions-scripts/__snapshots__/chrome-win32.png and /dev/null differ diff --git a/.github/actions-scripts/__snapshots__/edge-win32.png b/.github/actions-scripts/__snapshots__/edge-win32.png deleted file mode 100644 index 79140fe18..000000000 Binary files a/.github/actions-scripts/__snapshots__/edge-win32.png and /dev/null differ diff --git a/.github/actions-scripts/__snapshots__/firefox-darwin.png b/.github/actions-scripts/__snapshots__/firefox-darwin.png deleted file mode 100755 index ca2b341f0..000000000 Binary files a/.github/actions-scripts/__snapshots__/firefox-darwin.png and /dev/null differ diff --git a/.github/actions-scripts/__snapshots__/firefox-win32.png b/.github/actions-scripts/__snapshots__/firefox-win32.png deleted file mode 100644 index e3fc4e997..000000000 Binary files a/.github/actions-scripts/__snapshots__/firefox-win32.png and /dev/null differ diff --git a/.github/actions-scripts/__snapshots__/ie-win32.png b/.github/actions-scripts/__snapshots__/ie-win32.png deleted file mode 100644 index 7b74d9878..000000000 Binary files a/.github/actions-scripts/__snapshots__/ie-win32.png and /dev/null differ diff --git a/.github/actions-scripts/__snapshots__/puppeteer-darwin.png b/.github/actions-scripts/__snapshots__/puppeteer-darwin.png deleted file mode 100755 index 272f354d1..000000000 Binary files a/.github/actions-scripts/__snapshots__/puppeteer-darwin.png and /dev/null differ diff --git a/.github/actions-scripts/__snapshots__/safari-darwin.png b/.github/actions-scripts/__snapshots__/safari-darwin.png deleted file mode 100644 index 722b8d485..000000000 Binary files a/.github/actions-scripts/__snapshots__/safari-darwin.png and /dev/null differ diff --git a/.github/actions-scripts/polyfills-sync.js b/.github/actions-scripts/polyfills-sync.cjs similarity index 100% rename from .github/actions-scripts/polyfills-sync.js rename to .github/actions-scripts/polyfills-sync.cjs diff --git a/.github/actions-scripts/puppeteer.js b/.github/actions-scripts/puppeteer.js deleted file mode 100644 index d18be7e6a..000000000 --- a/.github/actions-scripts/puppeteer.js +++ /dev/null @@ -1,92 +0,0 @@ -const { readFileSync, writeFileSync, mkdirSync } = require('fs'); -const path = require('path'); -const { once } = require('events'); - -const puppeteer = require('puppeteer'); -const pixelmatch = require('pixelmatch'); -const { PNG } = require('pngjs'); - -const server = require('../../server'); - -async function test() { - const browser = await puppeteer.launch(); - const page = await browser.newPage(); - const artifactsPath = 'screenshot'; - const snapshotName = `puppeteer-${process.platform}.png`; - let error; - let pixelDifference; - let diff; - - if (!server.listening) await once(server, 'listening'); - - try { - page.on('console', msg => { - if (msg.type() === 'error') throw new Error(msg.text()); - }); - page.on('pageerror', err => { - throw err; - }); - - await page.goto(`http://127.0.0.1:${server.address().port}`, { - waitUntil: 'networkidle2', - }); - await page.setViewport({ width: 640, height: 1000 }); - await page.waitForTimeout(500); // Wait for resize to complete - await page.click('label[for="choices-single-custom-templates"]'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); - - mkdirSync(artifactsPath, { recursive: true }); - const imageBuffer = await page.screenshot({ - path: path.join(artifactsPath, snapshotName), - fullPage: true, - }); - - // compare with snapshot - const screenshot = PNG.sync.read(imageBuffer); - const snapshot = PNG.sync.read( - readFileSync(path.resolve(__dirname, `./__snapshots__/${snapshotName}`)), - ); - const { width, height } = screenshot; - diff = new PNG({ width, height }); - pixelDifference = pixelmatch( - screenshot.data, - snapshot.data, - diff.data, - width, - height, - { - threshold: 0.6, - }, - ); - } catch (err) { - console.error(err); - error = err; - } finally { - if (diff) { - writeFileSync(path.join(artifactsPath, 'diff-' + snapshotName), PNG.sync.write(diff)); - } - await Promise.all([ - browser.close(), - new Promise(resolve => server.close(resolve)), - ]); - } - - if (pixelDifference > 200) { - console.error( - `Snapshot is different from screenshot by ${pixelDifference} pixels`, - ); - process.exit(1); - } - if (error) process.exit(1); -} - -process.on('unhandledRejection', err => { - console.error(err); - process.exit(1); -}); -process.once('uncaughtException', err => { - console.error(err); - process.exit(1); -}); -setImmediate(test); diff --git a/.github/actions-scripts/selenium.js b/.github/actions-scripts/selenium.js deleted file mode 100644 index 1b69bac42..000000000 --- a/.github/actions-scripts/selenium.js +++ /dev/null @@ -1,155 +0,0 @@ -const path = require('path'); -const { readFileSync, writeFileSync, mkdirSync } = require('fs'); -const { once } = require('events'); - -const pixelmatch = require('pixelmatch'); -const { PNG } = require('pngjs'); -const { - Builder, - By, - Key, - until, - Capabilities, - logging, -} = require('selenium-webdriver'); - -const server = require('../../server'); - -async function test() { - let pixelDifference; - let error; - - let capabilities; - switch (process.env.BROWSER) { - case 'ie': - capabilities = Capabilities.ie(); - capabilities.set('ignoreProtectedModeSettings', true); - capabilities.set('ignoreZoomSetting', true); - capabilities.set('ie.options', { - enableFullPageScreenshot: true, - ensureCleanSession: true, - }); - break; - - case 'edge': - capabilities = Capabilities.edge(); - break; - - case 'safari': - capabilities = Capabilities.safari(); - capabilities.set('safari.options', { technologyPreview: false }); - break; - - case 'firefox': { - capabilities = Capabilities.firefox().setLoggingPrefs({ browser: 'ALL' }); - break; - } - case 'chrome': { - capabilities = Capabilities.chrome().setLoggingPrefs({ browser: 'ALL' }); - capabilities.set('chromeOptions', { - args: ['--headless', '--no-sandbox', '--disable-gpu'], - }); - break; - } - } - - let driver = await new Builder().withCapabilities(capabilities).build(); - - if (!server.listening) await once(server, 'listening'); - - try { - await driver.get(`http://127.0.0.1:${server.address().port}`); - - // wait for last choice to init - await driver.wait( - until.elementLocated(By.css('#reset-multiple ~ .choices__list')), - 10000, - 'waiting for all Choices instances to init', - ); - - // Resize window - await driver - .manage() - .window() - .maximize(); - await driver - .manage() - .window() - // magic numbers here to make sure all demo page are fit inside - .setRect({ x: 0, y: 0, width: 630, height: 4000 }); - - // and click on press space on it, so it should open choices - await driver - .findElement(By.css('#reset-multiple ~ .choices__list button')) - .sendKeys(Key.SPACE); - await driver.sleep(1000); - - // take screenshot - const image = await driver.takeScreenshot(); - const imageBuffer = Buffer.from(image, 'base64'); - - const snapshotName = `${process.env.BROWSER}-${process.platform}.png`; - const artifactsPath = 'screenshot'; - mkdirSync(artifactsPath, { recursive: true }); - - writeFileSync(path.join(artifactsPath, snapshotName), imageBuffer); - - // compare with snapshot - const screenshot = PNG.sync.read(imageBuffer); - const snapshot = PNG.sync.read( - readFileSync(path.resolve(__dirname, `./__snapshots__/${snapshotName}`)), - ); - const { width, height } = screenshot; - const diff = new PNG({ width, height }); - pixelDifference = pixelmatch( - screenshot.data, - snapshot.data, - diff.data, - width, - height, - { - threshold: 1, - }, - ); - writeFileSync(path.join(artifactsPath, 'diff.png'), PNG.sync.write(diff)); - - // getting console logs - // ensure no errors in console (only supported in Chrome currently) - if (process.env.BROWSER === 'chrome') { - const entries = await driver - .manage() - .logs() - .get(logging.Type.BROWSER); - if ( - Array.isArray(entries) && - entries.some(entry => entry.level.name_ === 'SEVERE') - ) - throw new Error(JSON.stringify(entries)); - } - } catch (err) { - console.error(err); - error = err; - } finally { - await Promise.all([ - driver.quit(), - new Promise(resolve => server.close(resolve)), - ]); - } - if (pixelDifference > 200) { - console.error( - `Snapshot is different from screenshot by ${pixelDifference} pixels`, - ); - process.exit(1); - } - if (error) process.exit(1); -} - -process.on('unhandledRejection', err => { - console.error(err); - process.exit(1); -}); -process.once('uncaughtException', err => { - console.error(err); - process.exit(1); -}); -setImmediate(test); diff --git a/.github/workflows/browsers.yml b/.github/workflows/browsers.yml index 20ba1bc9c..88b84da1e 100644 --- a/.github/workflows/browsers.yml +++ b/.github/workflows/browsers.yml @@ -1,131 +1,107 @@ -name: Browsers - +name: End-to-end tests (playwright) on: + push: + branches: [ main ] + paths: + - 'src/**' + - 'test-e2e/**' + - 'package-lock.json' + - '.browserslistrc' + - 'babel.config.json' + - 'public/index.html' + - 'public/**/index.html' + - '.github/workflows/browsers.yml' + - 'playwright.config.ts' pull_request: paths: - 'src/**' + - 'test-e2e/**' - 'package-lock.json' - '.browserslistrc' - - '.babelrc' - - 'webpack.config.*' + - 'babel.config.json' - 'public/index.html' - - '.github/actions-scripts/__snapshots__/**' + - 'public/**/index.html' - '.github/workflows/browsers.yml' - + - 'playwright.config.ts' jobs: - selenium: + test-e2e-playwright: + timeout-minutes: 60 strategy: fail-fast: false matrix: - os: [windows-latest, macos-latest] - browser: [edge, firefox, safari, chrome] + os: [windows-latest, macos-latest, ubuntu-latest] + browser: [chromium, firefox, webkit] exclude: - os: windows-latest - browser: safari - - os: macos-latest - browser: edge + browser: webkit + - os: windows-latest + browser: firefox - os: macos-latest - browser: chrome - # Safari workaround is not working in Catalina - - browser: safari - + browser: firefox runs-on: ${{ matrix.os }} steps: - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 + with: + fetch-depth: 1 - - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - uses: actions/setup-node@v2 - with: - node-version: 18.x - - - name: Cache node modules - uses: actions/cache@v2 - with: - path: ~/.npm - key: ${{ runner.OS }}-build-${{ matrix.browser }} - restore-keys: | - ${{ runner.OS }}-build-${{ env.cache-name }}- - ${{ runner.OS }}-build- - ${{ runner.OS }}- - - - run: | - npm ci - npm run build - env: - CYPRESS_INSTALL_BINARY: 0 - HUSKY_SKIP_INSTALL: true - - # install drivers - - name: Enable Safari Driver - run: | - # Workaround for `sudo safardriver --enable` not working: - # https://github.com/web-platform-tests/wpt/issues/19845 - # https://github.com/web-platform-tests/wpt/blob/master/tools/ci/azure/install_safari.yml - mkdir -p ~/Library/WebDriver/ - curl https://raw.githubusercontent.com/web-platform-tests/wpt/master/tools/ci/azure/com.apple.Safari.plist -o ~/Library/WebDriver/com.apple.Safari.plist - defaults write com.apple.Safari WebKitJavaScriptCanOpenWindowsAutomatically 1 - # sudo safaridriver --enable - if: matrix.browser == 'safari' - - - run: | - brew install --cask firefox - brew install geckodriver - if: matrix.browser == 'firefox' && matrix.os == 'macos-latest' + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' - - run: echo "$env:GeckoWebDriver" >> $GITHUB_PATH - if: matrix.browser == 'firefox' && matrix.os == 'windows-latest' + - name: Install dependencies + run: npm ci --no-audit + env: + HUSKY_SKIP_INSTALL: true - - run: echo "$env:EdgeWebDriver" >> $GITHUB_PATH - if: matrix.browser == 'edge' && matrix.os == 'windows-latest' + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - run: npx playwright install-deps - - run: echo "$env:ChromeWebDriver" >> $GITHUB_PATH - if: matrix.browser == 'chrome' && matrix.os == 'windows-latest' + - name: Run Playwright tests + run: npx playwright test --project=${{ matrix.browser }} + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshot-${{ matrix.os }} + path: test-results/**/*.png + - uses: actions/upload-artifact@v4 + if: '!cancelled()' + with: + name: blob-report-${{ matrix.os }}-${{ matrix.browser }} + path: blob-report/ + retention-days: 1 - - run: npm i --no-optional --no-audit selenium-webdriver pixelmatch pngjs - - run: node .github/actions-scripts/selenium.js - env: - BROWSER: ${{ matrix.browser }} - PORT: 0 - NODE_ENV: production # prevent watching + merge-reports: + # Merge reports after playwright-tests, even if some shards have failed + if: ${{ !cancelled() }} + needs: [test-e2e-playwright] - - uses: actions/upload-artifact@v2 - if: failure() - with: - name: screenshot-${{ matrix.browser }}-${{ matrix.os }} - path: screenshot - - puppeteer: - runs-on: macos-latest + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 1 - - name: Cache node modules - uses: actions/cache@v2 + + - uses: actions/setup-node@v4 with: - path: ~/.npm - key: ${{ runner.OS }}-build-puppeteer - restore-keys: | - ${{ runner.OS }}-build-puppeteer - - run: | - npm ci - npm run build - env: - CYPRESS_INSTALL_BINARY: 0 - HUSKY_SKIP_INSTALL: true - - run: npm i --no-optional --no-audit puppeteer pixelmatch pngjs - - run: node .github/actions-scripts/puppeteer.js - env: - PORT: 0 - NODE_ENV: production # prevent watching + node-version: 20 + - name: Install dependencies + run: npm ci + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v4 + with: + path: all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Merge into HTML Report + run: npx playwright merge-reports -c test-e2e/merge.config.ts ./all-blob-reports - - uses: actions/upload-artifact@v2 - if: failure() + - name: Upload HTML report + uses: actions/upload-artifact@v4 with: - name: screenshot-puppeteer-darwin - path: screenshot + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml deleted file mode 100644 index 68eea047d..000000000 --- a/.github/workflows/build-and-test.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Build and test - -on: - push: - branches: - - master - -jobs: - build-and-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - uses: actions/setup-node@v2 - with: - node-version: 18 - - name: Build and run all tests - run: | - npm ci - npm run build - npx bundlesize - npm run test:unit:coverage - npm run test:e2e - env: - CI: true - CI_REPO_NAME: ${{ github.event.repository.name }} - CI_REPO_OWNER: ${{ github.event.organization.login }} - CI_COMMIT_SHA: ${{ github.sha }} - GIT_COMMIT: ${{ github.sha }} - CI_BRANCH: ${{ github.head_ref }} - BUNDLESIZE_GITHUB_TOKEN: ${{secrets.BUNDLESIZE_GITHUB_TOKEN}} - FORCE_COLOR: 2 - HUSKY_SKIP_INSTALL: true - ## - ## Disabling for now. There does not appear to be a secure way to do this - ## with protected branches. See discussion: - ## https://github.community/t/how-to-push-to-protected-branches-in-a-github-action/16101 - ## - # - name: Commit built files - # run: | - # git config --local user.email "action@github.com" - # git config --local user.name "GitHub Action" - # git commit -m "Update build files 🏗" -a || echo "No changes to commit" && exit 0 - # - name: Push changes - # uses: ad-m/github-push-action@master - # with: - # github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload coverage to Codecov - run: bash <(curl -s https://codecov.io/bash) - -f ./coverage/lcov.info - -B ${{ github.head_ref }} - -C ${{ github.sha }} - -Z || echo 'Codecov upload failed' - env: - CI: true - GITLAB_CI: true # pretend we are GitLab CI, while Codecov adding support for Github Actions - CODECOV_ENV: github-action - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} diff --git a/.github/workflows/bundlesize.yml b/.github/workflows/bundlesize.yml index 905926123..08567b40d 100644 --- a/.github/workflows/bundlesize.yml +++ b/.github/workflows/bundlesize.yml @@ -1,8 +1,17 @@ name: Bundle size checks on: + push: + branches: [ main ] + paths: + - '.github/workflows/bundlesize.yml' + - 'src/scripts/**' + - 'src/styles/**' + - 'package-lock.json' + - '.browserslistrc' pull_request: paths: + - '.github/workflows/bundlesize.yml' - 'src/scripts/**' - 'src/styles/**' - 'package-lock.json' @@ -12,28 +21,29 @@ jobs: measure: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 1 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 + cache: 'npm' - - name: Install dependencies and build - run: | - npm ci - npm run build + - name: Install dependencies + run: npm ci --no-audit env: - CYPRESS_INSTALL_BINARY: 0 HUSKY_SKIP_INSTALL: true + - run: npm run build + # we don't need to build here, as even minized assets expected to be commited - - run: npx bundlesize + - run: npm run bundlesize env: - CI: true - BUNDLESIZE_GITHUB_TOKEN: ${{secrets.BUNDLESIZE_GITHUB_TOKEN}} + # token has expired, don't block the test + #CI: true + #BUNDLESIZE_GITHUB_TOKEN: ${{secrets.BUNDLESIZE_GITHUB_TOKEN}} CI_REPO_NAME: ${{ github.event.repository.name }} CI_REPO_OWNER: ${{ github.event.organization.login }} CI_COMMIT_SHA: ${{ github.event.after }} diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 455b534fd..b778a31e2 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -3,18 +3,18 @@ name: Deploy Pages on: release: types: [published] - workflow_dispatch: - + workflow_dispatch: + jobs: deploy-gh-pages: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 1 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 registry-url: https://registry.npmjs.org/ - name: Build run: | @@ -22,10 +22,9 @@ jobs: npm run build rm -rf public/test env: - CYPRESS_INSTALL_BINARY: 0 HUSKY_SKIP_INSTALL: true - name: Deploy - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PUBLISH_BRANCH: gh-pages diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index bec38f47d..aed499e7d 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -8,16 +8,15 @@ jobs: publish-npm: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 1 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 registry-url: https://registry.npmjs.org/ - run: npm ci env: - CYPRESS_INSTALL_BINARY: 0 HUSKY_SKIP_INSTALL: true - run: npm publish env: diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml deleted file mode 100644 index d3ebf5cbf..000000000 --- a/.github/workflows/e2e-tests.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: End-to-end tests - -on: - pull_request: - paths: - - 'src/**' - - 'package-lock.json' - - '.browserslistrc' - - '.babelrc' - - 'webpack.config.*' - - 'public/test/**' - - 'cypress/**' - - '.github/workflows/e2e-tests.yml' - -jobs: - test-e2e: - runs-on: ubuntu-latest - env: - CI: true - TERM: xterm-256color - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - - uses: actions/setup-node@v2 - with: - node-version: 18.x - - - name: Cache node modules - uses: actions/cache@v2 - with: - path: ~/.npm - key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.OS }}-build-${{ env.cache-name }}- - ${{ runner.OS }}-build- - ${{ runner.OS }}- - - - name: Get Cypress info - id: cypress-info - run: | - echo ::set-output name=version::$(jq -r .devDependencies.cypress ./package.json) - echo ::set-output name=cache::$(npx cypress cache path) - env: - CYPRESS_INSTALL_BINARY: 0 - - name: Cache Cypress cache - uses: actions/cache@v2 - with: - path: ${{ steps.cypress-info.outputs.cache }} - key: ${{ runner.OS }}-cypress-${{ steps.cypress-info.outputs.version }} - restore-keys: | - ${{ runner.OS }}-cypress-${{ steps.cypress-info.outputs.version }} - - - name: Install dependencies - run: npm ci - env: - HUSKY_SKIP_INSTALL: true - - - name: run Cypress (with or without recording) - # if we have ran out of free Cypress recordings, run Cypress with recording switched off - run: npm exec -- run-p --race start cypress:ci || npm exec -- run-p --race start cypress:run - env: - NODE_ENV: production # prevent watching - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - DEBUG: commit-info,cypress:server:record - # https://docs.cypress.io/guides/guides/continuous-integration.html#Environment-variables - COMMIT_INFO_BRANCH: ${{ github.head_ref }} - COMMIT_INFO_AUTHOR: ${{ github.event.sender.login }} - COMMIT_INFO_SHA: ${{ github.event.after }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cd4d4d8fa..7033ba972 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,29 +1,52 @@ name: Code linting on: + push: + branches: [ main ] + paths: + - '.github/workflows/lint.yml' + - 'src/scripts/**' + - 'src/*.ts' + - 'src/styles/**' + - 'test/**' + - 'test-e2e/**' + - 'package-lock.json' + - '.browserslistrc' + - '.eslintrc.json' + - '.editorconfig' + - '.prettierrc.json' + - '.stylelintrc.json' pull_request: paths: + - '.github/workflows/lint.yml' - 'src/scripts/**' + - 'src/*.ts' - 'src/styles/**' - - package-lock.json + - 'test/**' + - 'test-e2e/**' + - 'package-lock.json' - '.browserslistrc' + - '.eslintrc.json' + - '.editorconfig' + - '.prettierrc.json' + - '.stylelintrc.json' jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 1 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 + cache: 'npm' - name: Install dependencies - run: npm ci + run: npm ci --no-audit env: - CYPRESS_INSTALL_BINARY: 0 HUSKY_SKIP_INSTALL: true - name: run eslint diff --git a/.github/workflows/polyfills-sync.yml b/.github/workflows/polyfills-sync.yml index 7cb7193f6..ebfda1a8e 100644 --- a/.github/workflows/polyfills-sync.yml +++ b/.github/workflows/polyfills-sync.yml @@ -11,13 +11,13 @@ jobs: sync: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 1 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 - name: Check Polyfills documentation and settings sync - run: node .github/actions-scripts/polyfills-sync.js + run: node .github/actions-scripts/polyfills-sync.cjs diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 8e5d4bdbc..6491a4912 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -2,8 +2,7 @@ name: Release drafter on: push: - branches: - - master + branches: [ main ] jobs: update-draft-release: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 1737db8b4..8f43ca9e7 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,30 +1,48 @@ name: Unit tests on: + push: + branches: [ main ] + paths: + - '.github/workflows/unit-tests.yml' + - 'src/scripts/**' + - 'src/*.ts' + - 'test/**' + - 'package-lock.json' + - '.browserslistrc' + - 'babel.config.json' + - 'vitest.config.ts' pull_request: paths: + - '.github/workflows/unit-tests.yml' - 'src/scripts/**' - - package-lock.json + - 'src/*.ts' + - 'test/**' + - 'package-lock.json' - '.browserslistrc' + - 'babel.config.json' + - 'vitest.config.ts' jobs: test-unit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 1 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 + cache: 'npm' - name: Install dependencies - run: npm install --no-optional --no-audit --ignore-scripts + run: npm ci --no-audit env: - CYPRESS_INSTALL_BINARY: 0 HUSKY_SKIP_INSTALL: true + - run: npm run build + - run: npm run test:unit:coverage env: FORCE_COLOR: 2 diff --git a/.gitignore b/.gitignore index 7b0171fd2..38fd10048 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,17 @@ node_modules npm-debug.log .DS_Store .idea +.rollup.cache +tsconfig.tsbuildinfo +.npmrc +.run # Test tests/reports tests/results .nyc_output coverage -cypress/videos -cypress/screenshots +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.mocharc.yml b/.mocharc.yml deleted file mode 100644 index 40fed2d6f..000000000 --- a/.mocharc.yml +++ /dev/null @@ -1,8 +0,0 @@ -require: - - 'ts-node/register' - - './config/jsdom.js' -exit: true -spec: src/**/**/*.test.ts -extension: - - ts - - js diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 285bebcb1..000000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -message=":bookmark: Version %s" -git-tag-version=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 28193ca74..b427e2ae2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v12.13.1 +v20.16.0 \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index ab9f9780b..dde4fb41d 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,5 @@ { + "printWidth": 120, "singleQuote": true, "trailingComma": "all", "endOfLine": "lf", diff --git a/.stylelintrc.json b/.stylelintrc.json index 1f86e6bf7..d87c358e1 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,6 +1,7 @@ { "extends": "stylelint-config-standard-scss", "rules": { + "media-feature-range-notation": null, "declaration-block-no-redundant-longhand-properties": null } } \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 45cbd235e..a623962b6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -10,8 +10,5 @@ "github.vscode-pull-request-github", // needed for our configured debug configuration with Chrome "msjsdiag.debugger-for-chrome" - // Mocha recommended - https://mochajs.org/#mocha-sidebar-vs-code, - // but it's buggy - // "maty.vscode-mocha-sidebar" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 6bf8b709d..7592f2534 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,59 +12,5 @@ "webpack://Choices/*": "${workspaceFolder}/*" } }, - { - "type": "node", - "request": "launch", - "name": "Mocha Current File", - "program": "${workspaceFolder}/node_modules/mocha/bin/mocha", - "args": ["-u", "bdd", "--timeout", "999999", "--colors", "${file}"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "env": { - "NODE_ENV": "test" - } - }, - { - "type": "node", - "request": "launch", - "name": "Mocha All", - "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "args": ["-u", "bdd", "--timeout", "999999", "--colors"], - "console": "integratedTerminal", - "internalConsoleOptions": "openOnSessionStart", - "env": { - "NODE_ENV": "test" - } - }, - { - "type": "node", - "request": "launch", - "name": "Cypress Current File", - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/cypress", - "windows": { - "runtimeExecutable": "${workspaceFolder}\\node_modules\\.bin\\cypress.cmd" - }, - "runtimeArgs": [ - "run", - "--headed", - "--no-exit", - "--browser=electron", - "--port", - "9898", - "--spec" - ], - "protocol": "legacy", - "port": 9898, - "program": "${file}", - "console": "integratedTerminal", - "preLaunchTask": "buildAndWatch", - "internalConsoleOptions": "openOnSessionStart", - "timeout": 999999999999999, - "autoAttachChildProcesses": false, - "env": { - "NODE_ENV": "test" - // "DEBUG": "cypress:*" - } - } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 439eef583..5a669ecb2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,12 +23,6 @@ "public/assets": true, "**/coverage": true }, - // Mocha Sidebar settings - "mocha.env": { - "NODE_ENV": "test" - }, - "mocha.files.glob": "src/scripts/**/*.test.js", - "mocha.requires": ["@babel/register", "./config/jsdom.js"], // for Windows collaborators "files.eol": "\n", "files.encoding": "utf8", @@ -44,11 +38,6 @@ "npm.fetchOnlinePackageInfo": true, "eslint.packageManager": "npm", "json.schemas": [ - // Cypress related settings - https://docs.cypress.io/guides/tooling/intelligent-code-completion.html#Features-1 - { - "fileMatch": ["cypress.json"], - "url": "https://on.cypress.io/cypress.schema.json" - }, // Husky config file { "fileMatch": [".huskyrc"], diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5014b08f8..aa3cc2087 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -78,10 +78,5 @@ "script": "test:unit", "group": "test" }, - { - "type": "npm", - "script": "cypress:open", - "isBackground": true - } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..1f16c5c24 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,211 @@ +# Changelog + +## [11.0.2] + +### Features (from 11.0.0) +* Pass `getClassNames` as the 3rd argument to `callbackOnCreateTemplates` callback +* `duplicateItemsAllowed` option is now respected by `setChoices()` method [#855](https://github.com/Choices-js/Choices/issues/855) + +### Bug Fixes (from 11.0.0) +* Fix choice disable state wasn't considered when showing the "no choices to choose from" notice +* Fix regression where webpack doesn't permit importing scss/css @tagliala [#1193](https://github.com/Choices-js/Choices/issues/1193) +* Fix regression "no choices to choose from"/"no results found" notice did not reliably trigger. [#1185](https://github.com/Choices-js/Choices/issues/1185) [#1191](https://github.com/Choices-js/Choices/issues/1191) +* Fix regression of `UnhighlightItem` event not firing [#1173](https://github.com/Choices-js/Choices/issues/1173) +* Fix `clearChoices()` would remove items, and clear the search flag. +* Fixes for opt-group handling/rendering +* Fix `removeChoice()` did not properly remove a choice which was part of a group + +### Chore +* Add e2e tests for "no choices" behavior to match v10 + +## [11.0.1] (2024-08-30) + +### Bug Fixes (from 11.0.0) +* Fix the rendered item list was not cleared when `clearStore` was called. This impacted the on-form-reset and `refresh` features. + +### Chore +* Add e2e test for 'form reset' and 'on paste & search'. +* Cleanup adding classes to generated elements. + +## [11.0.0] (2024-08-28) + +### ⚠ BREAKING CHANGES +* Update polyfills to include `Element.prototype.replaceChildren` +* Number of internal APIs have changed + +### Bug Fixes (from 10.2.0) +* Reduce work done for `unhighlightAll` during on-click handler (batching in v11.0.0-rc8 would also have helped) [#522](https://github.com/Choices-js/Choices/issues/522) [#599](https://github.com/Choices-js/Choices/issues/599) +* Improve performance when rendering very large number of items and choices. Stuttering when stopping searching or selecting an item still happens depending on device and number of choices. + +## [11.0.0-rc8] (2024-08-23) + +### ⚠ BREAKING CHANGES +* Trigger a search event (with empty value and 0 resultCount) when search stops + +### Features +* `searchResultLimit` can be set to `-1` for no limit of search results to display. + +### Bug Fixes (from 10.2.0) +* Fix edge case where aria-label could be added twice +* Fix the page scrolls when you press 'space' on a single select input #1103 +* Update typescript definition for `removeActiveItems` to explicitly mark `excludedId` as optional #1116 + +### Chore +* Reduce the number of loops over choices when rendering search results, results in more compact code. +* Byte shave bundle sizes down + +## [11.0.0-rc7] (2024-08-19) + +### ⚠ BREAKING CHANGES +* Improve consistency of the `choice` event firing. `choice` event now occurs after the `addItem` event +* `enter` key now consistently opens/closes the dropdown instead of the behavior varying depending on backing element or internal state of the highlighted choice + +### Features +* Add `closeDropdownOnSelect` option, controls how the dropdown is close after selection is made. [#636](https://github.com/Choices-js/Choices/issues/636) [#973](https://github.com/Choices-js/Choices/issues/873) [#1012](https://github.com/Choices-js/Choices/issues/1012) +* Allow choices.js to be imported on nodejs, useful for tests and also server side rendering. As windows.document is by default not defined, the default template rendering will not function. The `callbackOnCreateTemplates` callback must be used. [#861](https://github.com/Choices-js/Choices/issues/861) + +### Bug Fixes (from 10.2.0) +* Improve various `[aria-*]` attribute handling for better lighthouse accessibility scores [#1169](https://github.com/Choices-js/Choices/issues/1169) +* Improve contrast on default CSS by darkening primary item selection color [#924](https://github.com/Choices-js/Choices/issues/924) + +### Bug Fixes (from 11.0.0RC6) +* Fix destroy&init of `choices.js` would lost track of data from the backing ``/`` initializes and selected values do not match the configured `choices.js` +* Fix legacy `placeholder` attribute support for `select-one` +* Fix `data-value` attribute on choices may not be correctly rendered into html + +### Chore +* Switch e2e tests from `puppeteer`/`selenium`/`cypress` to `playwright` +* Restructure end-to-end tests so html/script blocks are co-located to improve debugability +* Enable `@typescript-eslint/explicit-function-return-type` eslint rule + +## [11.0.0-rc6] (2024-08-12) + +### ⚠ BREAKING CHANGES +* Mutation APIs `setChoiceByValue`/`setChoices`/`setValue` now throw an error if the Choices instance was not initialized or multiple choices instances where initialized on the same element. Prevents bad internal states from triggering unexpected errors [#1129](https://github.com/Choices-js/Choices/issues/1129) + +### Features +* Improve performance of search/filtering with large number of choices. + +### Bug Fixes (from 10.2.0) +* Fix Choices does not accept an element from an iframe [#1057](https://github.com/Choices-js/Choices/issues/1057) +* Fix Choices was not disable in a `
` [#1132](https://github.com/Choices-js/Choices/issues/1132) +* Fix `silent` option does not silence warnings about unknown options [#1119](https://github.com/Choices-js/Choices/issues/1119) +* Fix documentation that suggests duplicateItemsAllowed works with select-multiple, when it only works for text. [#1123](https://github.com/Choices-js/Choices/issues/1123) +* Fix quadratic algorithm complexity (aka O(N^2) ) when filtering/search choices. +* Fix search results could be unexpectedly unstable, and that `fuseOptions.sortFn` was effectively ignored [#1106](https://github.com/Choices-js/Choices/issues/1106) + +### Bug Fixes (from 11.0.0RC1) +* Fix possible empty `aria-label` generation on remove item button +* Fix `clearChoices()` did not remove the actual selection options + +## [11.0.0-rc5] (2024-08-08) + +### ⚠ BREAKING CHANGES +* Update to using Fuse.js v7.0.0 +* Update choices.js package to be an ES module, and use '[subpath exports](https://nodejs.org/api/packages.html#subpath-exports)' to expose multiple versions (UMD, CJS or MTS bundles). +* Provide "fuse full" (default `choices.js`, ~20.36KB), or "fuse basic" (`choices.search-basic.js` ~19.31KB) or "prefix filter" (`choices.search-filter.js` ~15.27KB) based on how much Fuse.js is included. + +### Bug Fixes (from 10.2.0) +* Fix `select-one` placeholder could ignore the non-option placeholder configuration +* Remove typescript types for tests from distribution + +### Chore +* Reduce bundle size from ~24KB to ~20.36KB +* Switch bundler from `webpack` to `rollup` +* Switch test framework from `mocha` to `vitest` + +### Bug Fixes (from 11.0.0RC4) +* Fix `aria-describedby` was being assigned when it shouldn't be +* Fix check to ensure search was fully enabled for multiple select mode, as this functionality is hard-coded enabled elsewhere in the code base. + +## [11.0.0 RC3] (2024-08-04) + +### ⚠ BREAKING CHANGES +* For `select-one` and `select-multiple`, the placeholder value is pulled from `config.placeholderValue="..."` or `` as expected +* Fix adding user provided choices for `select-one` would not remove the existing item and result in a select-one with multiple items set. + +### Chore +* Remove unused code +* Use constant enum instead of repeating strings and type information +* For test html pages, prevent a failing `fetch()` from breaking the rest of the examples +* Tweak `_render()` loop to avoid duplicating has-changed checks + +## [11.0.0 RC1] (2024-08-02) + +### ⚠ BREAKING CHANGES + +* `allowHtml` now defaults to false. +* HTML escaping of choice/item labels should no longer double escape depending on allowHTML mode. +* Templates/text functions now escape `'` characters for display. +* `addItemText`/`uniqueItemText`/`customAddItemText` are now called with the `value` argument already escaped. +* Typescript classes for input data vs internal working data have been adjusted resulting in the `Choice`/`Group`/`Item` typescript classes have been renamed, and aliases left as required. + +### Features + +* `config.classNames` now accept arrays to support multiple classes. [#1121](https://github.com/Choices-js/Choices/issues/1121) [#1074](https://github.com/Choices-js/Choices/issues/1074) [#907](https://github.com/Choices-js/Choices/issues/907) [#832](https://github.com/Choices-js/Choices/issues/832) +* The original option list for the select is not destroyed, and all loaded choices are serialised to HTML for better compatibility with external javascript. [#1053](https://github.com/Choices-js/Choices/issues/1053) [#1023](https://github.com/Choices-js/Choices/issues/1023) +* New `singleModeForMultiSelect` feature to treat a `select-single` as if it was a `select-multiple` with a max item count of `1`, and still auto-close the dropdown and swap the active item on selection. [#1136](https://github.com/Choices-js/Choices/issues/1136) [#904](https://github.com/Choices-js/Choices/issues/904) +* `Remove item text` can be localized. +* Allow user-created choices for selects. [#1117](https://github.com/Choices-js/Choices/issues/1117) [#1114](https://github.com/Choices-js/Choices/issues/1114) + * User input is escaped by default. At the risk of XSS attacks this can be disabled by `allowHtmlUserInput`. +* Render options without a group even if groups are present. [#615](https://github.com/Choices-js/Choices/issues/615) [#1110](https://github.com/Choices-js/Choices/issues/1110) +* Read `data-labelclass`/`data-label-description` from `