diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index c8ac9648c..ff497a107 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -12,4 +12,6 @@ jobs: - uses: codespell-project/actions-codespell@master with: check_filenames: true - skip: ./.git,yarn.lock + # The a11y test file has a false positive and the ignore list does not work + # see https://github.com/opentripplanner/otp-react-redux/pull/436/checks?check_run_id=3369380014 + skip: ./.git,yarn.lock,./a11y/a11y.test.js diff --git a/a11y/a11y.test.js b/a11y/a11y.test.js index 1a3719e8c..c23576814 100644 --- a/a11y/a11y.test.js +++ b/a11y/a11y.test.js @@ -4,15 +4,39 @@ import path from 'path' import puppeteer from 'puppeteer' import execa from 'execa' +import { routes } from '../lib/components/app/responsive-webapp' + import { mockServer } from './mock-server' const OTP_RR_CONFIG_FILE_PATH = './config.yml' const OTP_RR_CONFIG_BACKUP_PATH = './config.non-test.yml' const OTP_RR_TEST_CONFIG_PATH = './a11y/test-config.yml' -let server +let browser, server +// These rules aren't relevant to this project +const disabledRules = [ + 'region', // Leaflet does not comply + 'meta-viewport', // Leaflet does not comply + 'page-has-heading-one' // Heading is provided by logo +] + +/** + * Runs a11y tests on a given OTP-RR path using the test build. Relies on + * the puppeteer browser running + */ +async function runAxeTestOnPath (otpPath) { + const page = await browser.newPage() + const filePath = `file://${path.resolve(__dirname, '../index-for-puppeteer.html')}#${otpPath}` + await Promise.all([ + page.goto(filePath), + page.waitForNavigation({ waitUntil: 'networkidle2' }) + ]) + + await expect(page).toPassAxeTests({ disabledRules }) + return page +} -beforeEach(() => { +beforeAll(async () => { // backup current config file if (fs.existsSync(OTP_RR_CONFIG_FILE_PATH)) { fs.renameSync( @@ -37,9 +61,11 @@ beforeEach(() => { server = mockServer.listen(MOCK_SERVER_PORT, () => { console.log(`Mock response server running on http://localhost:${MOCK_SERVER_PORT}`) }) + // Web security is disabled to allow requests to the mock OTP server + browser = await puppeteer.launch({args: ['--disable-web-security']}) }) -afterEach(async () => { +afterAll(async () => { fs.unlinkSync(OTP_RR_CONFIG_FILE_PATH) if (fs.existsSync(OTP_RR_CONFIG_BACKUP_PATH)) { fs.renameSync( @@ -49,35 +75,38 @@ afterEach(async () => { } console.log('Restored original OTP-RR config file') await server.close() - console.log('Closed mock server') + await browser.close() + console.log('Closed mock server and headless browser') }) -test('checks the test page with Axe', async () => { - jest.setTimeout(600000) - // These rules aren't relevant to this project - const disabledRules = [ - 'region', // Leaflet does not comply - 'meta-viewport', // Leaflet does not comply - 'page-has-heading-one' // Heading is provided by logo - ] +jest.setTimeout(600000) +routes.forEach(route => { + const {a11yIgnore, path: pathsToTest} = route + if (a11yIgnore) { + return + } - // Web security is disabled to allow requests to the mock OTP server - const browser = await puppeteer.launch({args: ['--disable-web-security']}) - let page = await browser.newPage() - // Test trip planner - await page.goto(`file://${path.resolve(__dirname, '../index-for-puppeteer.html')}#/?ui_activeSearch=0qoydlnut&ui_activeItinerary=0&fromPlace=1900%20Main%20Street%2C%20Houston%2C%20TX%2C%20USA%3A%3A29.750144%2C-95.370998&toPlace=800%20Congress%2C%20Houston%2C%20TX%2C%20USA%3A%3A29.76263%2C-95.362178&date=2021-08-04&time=08%3A14&arriveBy=false&mode=WALK%2CBUS%2CTRAM&showIntermediateStops=true&maxWalkDistance=1207&optimize=QUICK&walkSpeed=1.34&ignoreRealtimeUpdates=true&numItineraries=3&otherThanPreferredRoutesPenalty=900`) + if (Array.isArray(pathsToTest)) { + // Run test on each path in list. + pathsToTest.forEach(async (p) => { + test(`${p} should pass Axe Tests`, async () => runAxeTestOnPath(p)) + }) + } else { + // Otherwise run test on individual path + test(`${pathsToTest} should pass Axe Tests`, async () => runAxeTestOnPath(pathsToTest)) + } +}) - await expect(page).toPassAxeTests({ - disabledRules: disabledRules - }) - // Test stop viewer - page = await browser.newPage() - await page.goto(`file://${path.resolve(__dirname, '../index-for-puppeteer.html')}#/stop/exampleStop?ui_activeSearch=u9dwdhmyo&ui_activeItinerary=2&fromPlace=945 Columbia Street%2C Houston%2C TX%2C USA%3A%3A29.78881282532108%2C-95.3932571411133&toPlace=Hardy Street Yard%2C Houston%2C TX%2C USA%3A%3A29.772125846370574%2C-95.3551483154297&date=2021-08-18&time=17%3A07&arriveBy=false&mode=WALK%2CBUS%2CTRAM&showIntermediateStops=true&maxWalkDistance=1207&optimize=QUICK&walkSpeed=1.34&ignoreRealtimeUpdates=true&numItineraries=3&otherThanPreferredRoutesPenalty=900`) - await page.waitForTimeout(4000) - await page.click('.expansion-button') - await expect(page).toPassAxeTests({ - disabledRules: disabledRules - }) +test('Mocked Main Trip planner page should pass Axe Tests', async () => { + await runAxeTestOnPath('/?ui_activeSearch=0qoydlnut&ui_activeItinerary=0&fromPlace=1900%20Main%20Street%2C%20Houston%2C%20TX%2C%20USA%3A%3A29.750144%2C-95.370998&toPlace=800%20Congress%2C%20Houston%2C%20TX%2C%20USA%3A%3A29.76263%2C-95.362178&date=2021-08-04&time=08%3A14&arriveBy=false&mode=WALK%2CBUS%2CTRAM&showIntermediateStops=true&maxWalkDistance=1207&optimize=QUICK&walkSpeed=1.34&ignoreRealtimeUpdates=true&numItineraries=3&otherThanPreferredRoutesPenalty=900') +}) - await browser.close() +test('Mocked Stop Viewer and Dropdown should pass Axe tests', async () => { + jest.setTimeout(600000) + // Test stop viewer + const stopViewerPage = await runAxeTestOnPath('/stop/Agency') + await stopViewerPage.waitForTimeout(2000) + await stopViewerPage.click('.expansion-button') + await stopViewerPage.waitForTimeout(2000) + await expect(stopViewerPage).toPassAxeTests({ disabledRules }) }) diff --git a/a11y/mock-server.js b/a11y/mock-server.js index 47d56151d..cad363d84 100644 --- a/a11y/mock-server.js +++ b/a11y/mock-server.js @@ -3,6 +3,7 @@ const express = require('express') const PLAN_REALTIME = require('./mocks/plan.json') const STOPS_FIRST = require('./mocks/stops.json') const PARK_AND_RIDE = require('./mocks/pr.json') +const ROUTES = require('./mocks/routes.json') const STOP_VIEWER_STOPTIMES = require('./mocks/stopviewer/stoptimes.json') const STOP_VIEWER_STOP = require('./mocks/stopviewer/stop.json') const STOP_VIEWER_ROUTES = require('./mocks/stopviewer/routes.json') @@ -18,13 +19,16 @@ app.get('/otp/routers/default/index/stops', (req, res) => { app.get('/otp/routers/default/park_and_ride', (req, res) => { res.send(PARK_AND_RIDE) }) -app.get('/otp/routers/default/index/stops/exampleStop', (req, res) => { +app.get('/otp/routers/default/index/stops/Agency', (req, res) => { res.send(STOP_VIEWER_STOP) }) -app.get('/otp/routers/default/index/stops/exampleStop/routes', (req, res) => { +app.get('/otp/routers/default/index/stops/Agency/routes', (req, res) => { res.send(STOP_VIEWER_ROUTES) }) -app.get('/otp/routers/default/index/stops/exampleStop/stoptimes', (req, res) => { +app.get('/otp/routers/default/index/stops/Agency/stoptimes', (req, res) => { res.send(STOP_VIEWER_STOPTIMES) }) +app.get('/otp/routers/default/index/routes', (req, res) => { + res.send(ROUTES) +}) module.exports.mockServer = app diff --git a/a11y/mocks/routes.json b/a11y/mocks/routes.json new file mode 100644 index 000000000..81abba968 --- /dev/null +++ b/a11y/mocks/routes.json @@ -0,0 +1 @@ +[{"id":"Houston:41281","shortName":"002","longName":"BELLAIRE","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41282","shortName":"003","longName":"LANGLEY - LITTLE YORK","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41328","shortName":"072","longName":"WESTVIEW","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41329","shortName":"073","longName":"BELLFORT","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"1:16","longName":"Route 2 Baytown Central","mode":"BUS","color":"ffd342","agencyId":"140","agencyName":"Harris County","sortOrder":-999},{"id":"Houston:41324","shortName":"067","longName":"DAIRY ASHFORD","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"1:15","longName":"Route 1 Garth Rd","mode":"BUS","color":"5555eb","agencyId":"140","agencyName":"Harris County","sortOrder":-999},{"id":"Houston:41325","shortName":"068","longName":"BRAESWOOD","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41326","shortName":"070","longName":"MEMORIAL","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41327","shortName":"071","longName":"COTTAGE GROVE","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41293","shortName":"023","longName":"CLAY - W 43RD","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41339","shortName":"085","longName":"ANTOINE / WASHINGTON","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41335","shortName":"080","longName":"MLK / LOCKWOOD","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41336","shortName":"082","longName":"WESTHEIMER","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41337","shortName":"083","longName":"LEE ROAD - JFK","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41338","shortName":"084","longName":"BUFFALO SPEEDWAY","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41298","shortName":"029","longName":"CULLEN / HIRSCH","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41331","shortName":"076","longName":"EVERGREEN","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41332","shortName":"077","longName":"HOMESTEAD","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41299","shortName":"030","longName":"CLINTON / ELLA","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41333","shortName":"078","longName":"WAYSIDE","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41334","shortName":"079","longName":"IRVINGTON","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41294","shortName":"025","longName":"RICHMOND","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41295","shortName":"026","longName":"LONG POINT / CAVALCADE","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41296","shortName":"027","longName":"SHEPHERD","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41297","shortName":"028","longName":"OST - WAYSIDE","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41330","shortName":"075","longName":"ELDRIDGE","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41346","shortName":"098","longName":"BRIARGATE","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41347","shortName":"099","longName":"ELLA - FM 1960","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41348","shortName":"102","longName":"BUSH IAH EXPRESS","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41349","shortName":"108","longName":"VETERANS MEMORIAL EXPRESS","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41342","shortName":"088","longName":"SAGEMONT","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41343","shortName":"089","longName":"DACOMA","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41344","shortName":"096","longName":"VETERANS MEMORIAL","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41345","shortName":"097","longName":"SETTEGAST","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41340","shortName":"086","longName":"FM 1960 / IMPERIAL VALLEY","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41341","shortName":"087","longName":"SUNNYSIDE","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"1:23","longName":"Route 1 Garth Rd (early bird)","mode":"BUS","color":"5a5afa","agencyId":"140","agencyName":"Harris County","sortOrder":-999},{"id":"1:27","longName":"Baytown/La Porte Shuttle","mode":"BUS","color":"a26dad","agencyId":"140","agencyName":"Harris County","sortOrder":-999},{"id":"Houston:41357","shortName":"170","longName":"MISSOURI CITY EXPRESS","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41358","shortName":"171","longName":"FORTBEND TOWN CENTER","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41359","shortName":"209","longName":"KUYKENDAHL/SPRING P&R","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"1:28","longName":"Route 5 City of La Porte","mode":"BUS","color":"F0563d","agencyId":"140","agencyName":"Harris County","sortOrder":-999},{"id":"Houston:41353","shortName":"153","longName":"HARWIN EXPRESS","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999},{"id":"Houston:41354","shortName":"160","longName":"MEMORIAL CITY EXPRESS","mode":"BUS","color":"004080","agencyId":"HOU","agencyName":"Metropolitan Transit Authority of Harris County","sortOrder":-999}] \ No newline at end of file diff --git a/a11y/mocks/stopviewer/routes.json b/a11y/mocks/stopviewer/routes.json index 9316d3917..84332e678 100644 --- a/a11y/mocks/stopviewer/routes.json +++ b/a11y/mocks/stopviewer/routes.json @@ -1,5 +1,5 @@ [{ - "id": "exampleStop:1", + "id": "Agency:1", "shortName": "066", "longName": "QUITMAN", "mode": "BUS", diff --git a/a11y/mocks/stopviewer/stop.json b/a11y/mocks/stopviewer/stop.json index 9ceb8c410..73ef8320d 100644 --- a/a11y/mocks/stopviewer/stop.json +++ b/a11y/mocks/stopviewer/stop.json @@ -1,5 +1,5 @@ { - "id": "exampleStop", + "id": "Agency", "name": "White Oak Dr @ Oxford St", "lat": 29.78149, "lon": -95.391953, diff --git a/a11y/mocks/stopviewer/stoptimes.json b/a11y/mocks/stopviewer/stoptimes.json index b28210193..a83bb7313 100644 --- a/a11y/mocks/stopviewer/stoptimes.json +++ b/a11y/mocks/stopviewer/stoptimes.json @@ -1,12 +1,12 @@ [ { "pattern": { - "id": "exampleStop:1:01", + "id": "Agency:1:01", "desc": "040 to Glencrest St @ Airport Blvd (Houston:743)" }, "times": [ { - "stopId": "exampleStop:6478", + "stopId": "Agency:12345", "stopIndex": 44, "stopCount": 141, "scheduledArrival": 44621, @@ -27,7 +27,7 @@ "serviceAreaRadius": 0.0 }, { - "stopId": "exampleStop:6478", + "stopId": "Agency:12345", "stopIndex": 44, "stopCount": 141, "scheduledArrival": 44521, @@ -48,7 +48,7 @@ "serviceAreaRadius": 0.0 }, { - "stopId": "exampleStop:6478", + "stopId": "Agency:12345", "stopIndex": 44, "stopCount": 141, "scheduledArrival": 46321, diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 09eb22e4c..b9d3b5220 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -64,6 +64,7 @@ class ResponsiveWebapp extends Component { /** Lifecycle methods **/ + /* eslint-disable-next-line complexity */ componentDidUpdate (prevProps) { const { activeSearchId, @@ -285,6 +286,90 @@ const WebappWithRouter = withRouter( ) ) +// TODO: A number of these routes are ignored during a11y testing as no server mocks are available +export const routes = [ + { + exact: true, + path: [ + // App root + '/', + // Load app with preset lat/lon/zoom and optional router + // NOTE: All params will be cast to :id in matchContentToUrl due + // to a quirk with react-router. + // https://github.com/ReactTraining/react-router/issues/5870#issuecomment-394194338 + '/@/:latLonZoomRouter', + '/start/:latLonZoomRouter', + // Route viewer (and route ID). + '/route', + '/route/:id', + // Stop viewer (and stop ID). + '/stop', + '/stop/:id' + ], + shouldRenderWebApp: true + }, + { + a11yIgnore: true, + component: FavoritePlaceScreen, + path: [`${CREATE_ACCOUNT_PLACES_PATH}/:id`, `${PLACES_PATH}/:id`] + }, + { + a11yIgnore: true, + component: SavedTripScreen, + path: `${TRIPS_PATH}/:id` + }, + { + a11yIgnore: true, + children: , + exact: true, + path: ACCOUNT_PATH + }, + { + a11yIgnore: true, + children: , + exact: true, + path: CREATE_ACCOUNT_PATH + }, + { + a11yIgnore: true, + // This route lets new or existing users edit or set up their account. + component: UserAccountScreen, + path: [`${CREATE_ACCOUNT_PATH}/:step`, ACCOUNT_SETTINGS_PATH] + }, + { + getContextComponent: (components) => frame(components.TermsOfService), + path: TERMS_OF_SERVICE_PATH + }, + { + getContextComponent: (components) => frame(components.TermsOfStorage), + path: TERMS_OF_STORAGE_PATH + }, + { + a11yIgnore: true, + component: SavedTripList, + path: TRIPS_PATH + }, + { + a11yIgnore: true, + // This route is called immediately after login by Auth0 + // and by the onRedirectCallback function from /lib/util/auth.js. + // For new users, it displays the account setup form. + // For existing users, it takes the browser back to the itinerary search prior to login. + component: AfterSignInScreen, + path: '/signedin' + }, + { + a11yIgnore: true, + component: PrintLayout, + path: '/print' + }, + { + a11yIgnore: true, + component: PrintFieldTripLayout, + path: '/printFieldTrip' + } +] + /** * The routing component for the application. * This is the top-most "standard" component, @@ -316,73 +401,26 @@ class RouterWrapperWithAuth0 extends Component { basename={routerConfig && routerConfig.basename} history={history}> - } - /> - - - - - - - - - - - - - - - + {routes.map((props, index) => { + const { + getContextComponent, + shouldRenderWebApp, + ...routerProps + } = props + + return ( + + : undefined + } + {...routerProps} /> + ) + })} {/* For any other route, simply return the web app. */} } diff --git a/lib/components/viewers/realtime-status-label.js b/lib/components/viewers/realtime-status-label.js index c2eecbf49..8525e62a2 100644 --- a/lib/components/viewers/realtime-status-label.js +++ b/lib/components/viewers/realtime-status-label.js @@ -67,7 +67,7 @@ const RealtimeStatusLabel = ({ const isEarlyOrLate = status === REALTIME_STATUS.EARLY || status === REALTIME_STATUS.LATE // Use a default background color if the status object doesn't set a color // (e.g. for 'Scheduled' status), but only in withBackground mode. - const color = STATUS[status].color || (withBackground && '#6D6C6Cb') + const color = STATUS[status].color || (withBackground && '#6D6C6C') // Render time if provided. let renderedTime if (time) { diff --git a/package.json b/package.json index 87479045c..0e8451e3e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "A library for writing modern OpenTripPlanner-compatible multimodal journey planning web applications using React and Redux", "main": "build/index.js", "scripts": { - "a11y-test": "mastarm test a11y --force-exit", + "a11y-test": "mastarm test a11y --runInBand --force-exit", "build": "mastarm build --env production", "cover": "mastarm test -e test --coverage", "jest": "mastarm test -e test __tests__",