diff --git a/.eslintrc.js b/.eslintrc.js index f37017be5..212929d23 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,7 +20,11 @@ module.exports = { }, }, { - files: ["tests/a11y/**/*.js", "tests/e2e/**/*.js"], + files: [ + "tests/a11y/**/*.js", + "tests/e2e/**/*.js", + "tests/load-times/**/*.js", + ], extends: ["airbnb-base", "prettier", "plugin:cypress/recommended"], env: { jest: true, diff --git a/.github/workflows/code-standards.yaml b/.github/workflows/code-standards.yaml index ce6aa17db..491e49e7b 100644 --- a/.github/workflows/code-standards.yaml +++ b/.github/workflows/code-standards.yaml @@ -58,6 +58,7 @@ jobs: name: PHP lint runs-on: ubuntu-latest needs: [should-test] + if: needs.should-test.outputs.yes == 'true' steps: - name: checkout @@ -75,20 +76,17 @@ jobs: echo "bin-dir=${{ github.workspace }}/$(composer config bin-dir)" >> $GITHUB_OUTPUT - name: cache composer caches - if: needs.should-test.outputs.yes == 'true' uses: actions/cache@v3 with: path: ${{ steps.composer-paths.outputs.cache-dir }} key: composer-cache-${{ hashFiles('composer.lock') }} - name: install dependencies - if: needs.should-test.outputs.yes == 'true' env: COMPOSER_NO_DEV: 0 run: composer install - name: run phpcs - if: needs.should-test.outputs.yes == 'true' run: | echo "::add-matcher::${{ github.workspace }}/.github/workflows/problem-matcher-phpcs.json" vendor/bin/phpcs --report=checkstyle @@ -97,6 +95,7 @@ jobs: name: JS lint runs-on: ubuntu-latest needs: [should-test] + if: needs.should-test.outputs.yes == 'true' steps: - name: checkout @@ -118,13 +117,13 @@ jobs: run: echo "::add-matcher::${{ github.workspace }}/.github/workflows/problem-matcher-eslint.json" - name: run eslint - if: needs.should-test.outputs.yes == 'true' run: npm run js-lint style-lint: name: SCSS lint runs-on: ubuntu-latest needs: [should-test] + if: needs.should-test.outputs.yes == 'true' steps: - name: checkout @@ -144,13 +143,13 @@ jobs: # https://stylelint.io/user-guide/options#formatter # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions - name: run stylelint - if: needs.should-test.outputs.yes == 'true' run: npm run style-lint -- -f github php-unit-tests: name: PHP unit tests runs-on: ubuntu-latest needs: [should-test] + if: needs.should-test.outputs.yes == 'true' steps: - name: checkout @@ -169,25 +168,21 @@ jobs: echo "bin-dir=${{ github.workspace }}/$(composer config bin-dir)" >> $GITHUB_OUTPUT - name: cache composer caches - if: needs.should-test.outputs.yes == 'true' uses: actions/cache@v3 with: path: ${{ steps.composer-paths.outputs.cache-dir }} key: composer-cache-${{ hashFiles('composer.lock') }} - name: install dependencies - if: needs.should-test.outputs.yes == 'true' env: COMPOSER_NO_DEV: 0 run: composer install - name: run unit tests - if: needs.should-test.outputs.yes == 'true' run: | vendor/bin/phpunit --coverage-clover coverage.xml - name: store coverage output - if: needs.should-test.outputs.yes == 'true' uses: actions/upload-artifact@v3 with: name: coverage-report @@ -197,17 +192,15 @@ jobs: min-code-coverage: name: "90% code coverage" runs-on: ubuntu-latest - needs: [php-unit-tests, should-test] + needs: [php-unit-tests] steps: - name: get coverage output - if: needs.should-test.outputs.yes == 'true' uses: actions/download-artifact@v3 with: name: coverage-report - name: 90% code coverage - if: needs.should-test.outputs.yes == 'true' id: test-coverage uses: johanvanhelden/gha-clover-test-coverage-check@v1 with: @@ -215,58 +208,73 @@ jobs: filename: coverage.xml metric: statements - end-to-end-tests: - name: end-to-end tests + build-drupal-image: + name: build Drupal image runs-on: ubuntu-latest needs: [should-test] + if: needs.should-test.outputs.yes == 'true' steps: - name: checkout - if: needs.should-test.outputs.yes == 'true' - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: setup image cacheing + uses: actions/cache@v3 + id: cache + with: + key: drupal-image-${{ hashFiles('composer.lock','web/sites/example.settings.dev.php') }} + path: /tmp/image.tar - name: Set up Docker Buildx - if: needs.should-test.outputs.yes == 'true' - id: buildx + if: steps.cache.outputs.cache-hit != 'true' uses: docker/setup-buildx-action@v3 - - name: build site container - if: needs.should-test.outputs.yes == 'true' - uses: docker/build-push-action@v2 + - name: build and export + if: steps.cache.outputs.cache-hit != 'true' + uses: docker/build-push-action@v5 with: - builder: ${{ steps.buildx.outputs.name }} - push: false - load: true - tags: 18f-zscaler-drupal:10-apache - file: ./Dockerfile.dev context: . - cache-from: type=gha - cache-to: type=gha,mode=max + file: ./Dockerfile.dev + tags: 18f-zscaler-drupal:10-apache + outputs: type=docker,dest=/tmp/image.tar + + end-to-end-tests: + name: end-to-end tests + runs-on: ubuntu-latest + needs: [build-drupal-image] + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: setup image cacheing + uses: actions/cache@v3 + id: cache + with: + key: drupal-image-${{ hashFiles('composer.lock','web/sites/example.settings.dev.php') }} + path: /tmp/image.tar - name: start the site - if: needs.should-test.outputs.yes == 'true' - run: docker compose up -d + run: | + docker load --input /tmp/image.tar + docker compose up -d # Give the containers a moment to settle. - name: wait a tick - if: needs.should-test.outputs.yes == 'true' run: sleep 10 - name: populate the site - if: needs.should-test.outputs.yes == 'true' run: | cp web/sites/example.settings.dev.php web/sites/settings.dev.php make install-site - name: Cypress run - if: needs.should-test.outputs.yes == 'true' uses: cypress-io/github-action@v6 with: project: tests/e2e cache-key: cypress-e2e-${{ hashFiles('package-lock.json') }} - name: save screenshots - if: needs.should-test.outputs.yes == 'true' uses: actions/upload-artifact@v3 with: name: screenshots @@ -275,49 +283,71 @@ jobs: accessibility-tests: name: accessibility tests runs-on: ubuntu-latest - needs: [should-test] + needs: [build-drupal-image] steps: - name: checkout - if: needs.should-test.outputs.yes == 'true' uses: actions/checkout@v3 - - name: Set up Docker Buildx - if: needs.should-test.outputs.yes == 'true' - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: build site container - if: needs.should-test.outputs.yes == 'true' - uses: docker/build-push-action@v2 + - name: setup image cacheing + uses: actions/cache@v3 + id: cache with: - builder: ${{ steps.buildx.outputs.name }} - push: false - load: true - tags: 18f-zscaler-drupal:10-apache - file: ./Dockerfile.dev - context: . - cache-from: type=gha - cache-to: type=gha,mode=max + key: drupal-image-${{ hashFiles('composer.lock','web/sites/example.settings.dev.php') }} + path: /tmp/image.tar - name: start the site - if: needs.should-test.outputs.yes == 'true' - run: docker compose up -d + run: | + docker load --input /tmp/image.tar + docker compose up -d # Give the containers a moment to settle. - name: wait a tick - if: needs.should-test.outputs.yes == 'true' run: sleep 10 - name: populate the site - if: needs.should-test.outputs.yes == 'true' run: | cp web/sites/example.settings.dev.php web/sites/settings.dev.php make install-site - name: Cypress run - if: needs.should-test.outputs.yes == 'true' uses: cypress-io/github-action@v6 with: project: tests/a11y cache-key: cypress-a11y-${{ hashFiles('package-lock.json') }} + + page-load-time-tests: + name: page load time tests + runs-on: ubuntu-latest + needs: [build-drupal-image] + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: setup image cacheing + uses: actions/cache@v3 + id: cache + with: + key: drupal-image-${{ hashFiles('composer.lock','web/sites/example.settings.dev.php') }} + path: /tmp/image.tar + + - name: start the site + run: | + docker load --input /tmp/image.tar + docker compose up -d + + # Give the containers a moment to settle. + - name: wait a tick + run: sleep 10 + + - name: populate the site + run: | + cp web/sites/example.settings.dev.php web/sites/settings.dev.php + make install-site + + - name: Cypress run + uses: cypress-io/github-action@v6 + with: + project: tests/load-times + cache-key: cypress-a11y-${{ hashFiles('package-lock.json') }} diff --git a/Makefile b/Makefile index 157ff9a64..ebb899d9d 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,10 @@ ee: end-to-end-test end-to-end-test: ## Run end-to-end tests in Cypress. (alias ee) npx cypress run --project tests/e2e +lt: load-time-test +load-time-test: ## Run page load time tests in Cypress (alias lt) + npx cypress run --project tests/load-times + u: unit-test unit-test: ## Run PHP unit tests docker compose exec drupal phpunit --coverage-html /coverage @@ -121,4 +125,4 @@ style-format: ## Format your Sass code according to our style code. ### Composer management ci: composer-install composer-install: ## Installs dependencies from lock file - docker compose exec drupal composer install \ No newline at end of file + docker compose exec drupal composer install diff --git a/tests/load-times/cypress.config.js b/tests/load-times/cypress.config.js new file mode 100644 index 000000000..f854fbb44 --- /dev/null +++ b/tests/load-times/cypress.config.js @@ -0,0 +1,9 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + e2e: { + baseUrl: "http://localhost:8080", + screenshotOnRunFailure: false, + supportFile: false, + }, +}); diff --git a/tests/load-times/cypress/e2e/page-load-time.cy.js b/tests/load-times/cypress/e2e/page-load-time.cy.js new file mode 100644 index 000000000..04aa968c1 --- /dev/null +++ b/tests/load-times/cypress/e2e/page-load-time.cy.js @@ -0,0 +1,56 @@ +describe("pages load", () => { + it("average less than 3 seconds, max less than 5 seconds", () => { + const pages = [ + { name: "location page 1", url: "/local/OKX/33/35/Hoboken" }, + { name: "location page 2", url: "/local/LOX/155/45/Vernon" }, + { name: "location page 3", url: "/local/LOT/75/73/Chicago" }, + { name: "location page 4", url: "/local/HGX/65/97/Houston" }, + { name: "location page 5", url: "/local/PSR/159/58/Guadalupe" }, + ]; + + const measurements = []; + + // Measures the first page in the list of pages. When it's done, if there + // are any pages left in the list, does it again. This helps ensure our + // tests are running synchronously rather than in parallel so our timers + // will be a little more accurate. + const measurePage = () => { + const { name, url } = pages.shift(); + performance.mark(name); + cy.visit(url); + + // We want to wait until the current conditions are found on the page. + // Note that this can inflate the timing numbers a bit because Cypress + // will retry a few times with a short wait in between. Return the result + // because it's a Cypress chainable object and the caller can use it to + // know when we're all finished. + return cy.get(".weather-gov-current-conditions").then(() => { + const measurement = performance.measure(name, { start: name }); + measurements.push({ + name, + url, + measurement: Math.round(measurement.duration), + }); + + // Whether there are more pages or not, return a Cypress chainable. + if (pages.length > 0) { + return measurePage(); + } + return cy.wrap(); + }); + }; + + // Meaure the pages. When they're completely finished, then we can get the + // max and average and make our assertions. + measurePage().then(() => { + const times = measurements.map(({ measurement }) => measurement); + const total = times.reduce((prev, now) => prev + now, 0); + + const average = total / times.length; + const max = Math.max(...times); + + cy.wrap(average).should("be.lessThan", 3000); + cy.wrap(max).should("be.lessThan", 5000); + }); + }); +});