diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..95876263b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,107 @@ +# This workflow will do the following when a pull request is made against the main branch: +# 1. Clone base repository (apexcharts/apexcharts.js), install npm packages, build samples, and then generate e2e snapshots +# 2. Clone head repository (the repository with the code in the pull request), install npm packages, build samples, get snapshots generated from the base repository, run tests, and then generate an apexcharts build +# Test coverage results, base repository snapshots, head repository snapshots and diffs, sample HTML, and the apexcharts build are all uploaded to the workflow artifacts +# The diffs, build, and coverage results get uploaded even if the test fails for manual inspection and to make debugging easier + +name: Node.js CI + +on: + pull_request: + branches: [ main ] + +jobs: + build: + name: Test & Build + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node-version: [18.x, 20.x, 22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - name: Create base repository artifacts folder + run: mkdir -p ${{ runner.temp }}/artifacts/base-repository + - name: Create head repository artifacts folder + run: mkdir -p ${{ runner.temp }}/artifacts/head-repository + + - name: Checkout base repository + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.base.repo.full_name }} + ref: ${{ github.event.pull_request.base.ref }} + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install base repository's packages + run: npm ci + - name: Generate base repository's samples + run: npm run build:samples + - name: Copy Base Repository Samples To Artifacts Folder + run: cp -r samples/vanilla-js ${{ runner.temp }}/artifacts/base-repository/samples + - name: Generate base repository's e2e snapshots + run: npm run e2e:update + - name: Copy Base Repository e2e Snapshots To Artifacts Folder + run: cp -r tests/e2e/snapshots ${{ runner.temp }}/artifacts/base-repository/snapshots + + - name: Checkout head repository + uses: actions/checkout@v4 + - name: Install head repository's packages + run: npm ci + - name: Generate head repository's samples + run: npm run build:samples + - name: Copy Head Repository Samples To Artifacts Folder + run: cp -r samples/vanilla-js ${{ runner.temp }}/artifacts/head-repository/samples + - name: Delete snapshots folder + run: rm -r tests/e2e/snapshots + - name: Copy snapshots from base repository + run: cp -r ${{ runner.temp }}/artifacts/base-repository/snapshots tests/e2e + - name: Run tests + run: npm run test:ci + - name: Copy Head Repository Diffs To Artifacts Folder + if: '!cancelled()' + run: cp -r tests/e2e/diffs ${{ runner.temp }}/artifacts/head-repository/diffs + - name: Build apexcharts + if: '!cancelled()' + run: npm run build --if-present + - name: Copy Head Repository Build To Artifacts Folder + if: '!cancelled()' + run: cp -r dist ${{ runner.temp }}/artifacts/head-repository/build + - name: Copy Head Repository Test Coverage To Artifacts Folder + if: '!cancelled()' + run: cp -r coverage ${{ runner.temp }}/artifacts/head-repository/coverage + - name: Upload Artifacts + if: '!cancelled()' + uses: actions/upload-artifact@v4 + with: + name: node${{ matrix.node-version }}${{ runner.os }}results + path: ${{ runner.temp }}/artifacts + + test-reproducibility: + name: Test Reproducibility + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Node ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install packages + run: npm ci + - name: Build samples + run: npm run build:samples + - name: Generate snapshots + run: npm run e2e:update + - name: Run tests + run: npm run test:ci + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..b268a6675 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,20 @@ +name: Lint + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 22.x + uses: actions/setup-node@v4 + with: + node-version: 22.x + - run: npm ci + - name: Lint + run: npm run lint \ No newline at end of file diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml deleted file mode 100644 index ad8d7f198..000000000 --- a/.github/workflows/node.js.yml +++ /dev/null @@ -1,32 +0,0 @@ -# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Node.js CI - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [12.x, 14.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - run: npm ci - - run: npm run lint - - run: npm test - - run: npm run build --if-present diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index a0581309b..3053d500a 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -21,3 +21,4 @@ Please delete options that are not relevant. - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes +- [ ] My branch is up to date with any changes from the main branch diff --git a/package.json b/package.json index c32ff6c96..becac55e9 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,11 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "test": "npm run e2e && npm run unit", + "test:ci": "npm run e2e:ci && npm run unit", "unit": "jest tests/unit/", "e2e": "node tests/e2e/samples.js test", "e2e:update": "node tests/e2e/samples.js update", + "e2e:ci": "node tests/e2e/samples.js test:ci", "build:samples": "node samples/source/index.js generate" }, "dependencies": { @@ -105,4 +107,4 @@ "visualizations", "data" ] -} +} \ No newline at end of file diff --git a/tests/e2e/samples.js b/tests/e2e/samples.js index ea5715de2..cc3a0692b 100644 --- a/tests/e2e/samples.js +++ b/tests/e2e/samples.js @@ -24,6 +24,12 @@ class TestError extends Error { } } +class MissingSnapshotError extends Error { + constructor(message) { + super(message) + } +} + async function processSample(page, sample, command) { const relPath = `${sample.dirName}/${sample.fileName}` const vanillaJsHtml = `${rootDir}/samples/vanilla-js/${relPath}.html` @@ -145,7 +151,16 @@ async function processSample(page, sample, command) { // Compare screenshot to the original and throw error on differences const testImg = PNG.sync.read(testImgBuffer) // BUG: copy if original image doesn't exist and report in test results? - const originalImg = PNG.sync.read(fs.readFileSync(originalImgPath)) + let originalImg; + try { + originalImg = PNG.sync.read(fs.readFileSync(originalImgPath)) + } catch (e) { + if (e.code === 'ENOENT') { + //The file could not be found so throw a MissingSnapshotError + throw new MissingSnapshotError(relPath) + } + throw e + } const { width, height } = testImg const diffImg = new PNG({ width, height }) @@ -219,7 +234,7 @@ async function updateBundle(config) { } } -async function processSamples(command, paths) { +async function processSamples(command, paths, isCI) { const startTime = Date.now() await updateBundle(builds['web-umd-dev']) @@ -241,6 +256,7 @@ async function processSamples(command, paths) { let numCompleted = 0 const failedTests = [] // {path, error} + const testsMissingSnapshots = [] // 'pathForSnapshot' // Build a list of samples to process let samples = extractSampleInfo() @@ -278,10 +294,14 @@ async function processSamples(command, paths) { try { await processSample(page, sample, command) } catch (e) { - failedTests.push({ - path: `${sample.dirName}/${sample.fileName}`, - error: e, - }) + if (e instanceof MissingSnapshotError) { + testsMissingSnapshots.push(e.message) + } else { + failedTests.push({ + path: `${sample.dirName}/${sample.fileName}`, + error: e, + }) + } } numCompleted++ if (!process.stdout.isTTY) { @@ -310,6 +330,13 @@ async function processSamples(command, paths) { chalk.green.bold(`${samples.length} tests completed in ${duration} sec.`) ) + if (testsMissingSnapshots.length > 0) { + console.log(chalk.yellow.bold(`${testsMissingSnapshots.length} tests were missing snapshots to compare against. Those tests are:`)) + for (const testMissingSnapshot of testsMissingSnapshots) { + console.log(chalk.yellow.bold(`${testMissingSnapshot}\n`)) + } + } + if (failedTests.length > 0) { console.log(chalk.red.bold(`${failedTests.length} tests failed`)) } @@ -340,18 +367,32 @@ async function processSamples(command, paths) { throw new Error('Code coverage report failed to generate') } } + + if (failedTests.length > 0 && isCI) { + //Exit with error code to fail CI if a test failed + process.exit(1) + } } // Run as 'node samples.js ...' -// Supports two commands: +// Supports three commands: // - 'test' for running e2e tests +// - 'test:ci' for running e2e tests in CI - 'test:ci' exits with status code 1 if a test fails, while 'test' always exits with status code 0 // - 'update' for updating samples screenshots used for e2e tests comparison // Path options have the format 'bar/basic-bar'. Paths are optional for 'test' command. // For 'update' command 'all' path can be used to update all screenshots. -const command = process.argv[2] -if (['update', 'test'].includes(command)) { - processSamples(command, process.argv.slice(3)) - .catch((e) => console.log(e)) +const commandInput = process.argv[2] +if (['update', 'test', 'test:ci'].includes(commandInput)) { + const isCI = commandInput === 'test:ci' + const command = isCI ? 'test' : commandInput + processSamples(command, process.argv.slice(3), isCI) + .catch((e) => { + console.error(e) + if (isCI) { + //Exit with error code to fail CI if something failed + process.exit(1) + } + }) .then(() => { if (browser) { return browser.close()