diff --git a/.gitignore b/.gitignore
index 91191804f..83c085bc2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -112,9 +112,6 @@ storybook-static
# Lighthouse
.lighthouseci
-#VSCode
-.vscode
-
# Ansible
deploy/ansible-vault-password.txt
@@ -124,4 +121,4 @@ deploy/provision.retry
# Migration generated files
server/migrations/pg_dump_test_plan_target.sql
-server/migrations/test_plan_target_id.csv
\ No newline at end of file
+server/migrations/test_plan_target_id.csv
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..8ce0f7999
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,19 @@
+{
+ "version": "1.0.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Jest Server Debug Current Test File",
+ //"env": { "NODE_ENV": "test" },
+ "envFile": "${workspaceFolder}/config/test.env",
+ "program": "${workspaceFolder}/server/node_modules/.bin/jest",
+ "args": ["${fileBasenameNoExtension}", "--config", "server/jest.config.js"],
+ "console": "integratedTerminal",
+ "disableOptimisticBPs": true,
+ "windows": {
+ "program": "${workspaceFolder}/server/node_modules/jest/bin/jest"
+ }
+ }
+ ]
+}
diff --git a/deploy/README.md b/deploy/README.md
index 5acdb36ad..71dd0d5d3 100644
--- a/deploy/README.md
+++ b/deploy/README.md
@@ -58,12 +58,14 @@ To deploy this project to server:
- Run `ssh aria-at-app-sandbox.bocoup.com` and confirm you can connect.
- Confirm that `sudo su` successfully switches you to the root user. You will need to enter the sodoer password you chose during your Bocoup onboarding. This password will be required when deploying to the Sandbox.
3. Obtain a copy of the `ansible-vault-password.txt` file in LastPass and place it in the directory which contains this document.
-4. Install [Ansible](https://www.ansible.com/) version 2.8. Instructions for macOS are as follows:
- - Install Python 2.7, which is not included by default on recent macOS versions.
- - Verify that Pip, Python's package manager, is using Python 2.7 by running `pip --version`.
- - Install Ansible at the specific 2.8 version: `pip install ansible==2.8.20`
- - Run `ansible --version` to verify your ansible is on version 2.8.
- - You may need to run `ansible-galaxy collection install ansible.posix --ignore-certs` as well.
+4. Install [Ansible](https://www.ansible.com/) version 2.11. Instructions for macOS are as follows:
+ - Install Ansible at the specific 2.11 version: `python3 -m pip install --user ansible-core==2.11.1`
+ - Add the following line to your `~/.zshrc` file, changing the path below to match where Python installs Ansible for you:
+ ```
+ export PATH=$PATH:/Users/Luigi/Library/Python/3.9/bin
+ ```
+ - Run `source ~/.zshrc` to refresh your shell.
+ - Run `ansible --version` to verify your ansible is on version 2.11.
5. Execute the following command from the deploy directory:
- Sandbox:
```
diff --git a/server/app.js b/server/app.js
index c6f7f60e3..719a74662 100644
--- a/server/app.js
+++ b/server/app.js
@@ -2,19 +2,12 @@ const express = require('express');
const bodyParser = require('body-parser');
const cacheMiddleware = require('apicache').middleware;
const proxyMiddleware = require('rawgit/lib/middleware');
-const { ApolloServer } = require('apollo-server-express');
-const {
- ApolloServerPluginLandingPageGraphQLPlayground
-} = require('apollo-server-core');
const { session } = require('./middleware/session');
const embedApp = require('./apps/embed');
const authRoutes = require('./routes/auth');
const testRoutes = require('./routes/tests');
const path = require('path');
-const graphqlSchema = require('./graphql-schema');
-const getGraphQLContext = require('./graphql-context');
-const resolvers = require('./resolvers');
-
+const apolloServer = require('./graphql-server');
const app = express();
// test session
@@ -23,15 +16,8 @@ app.use(bodyParser.json());
app.use('/auth', authRoutes);
app.use('/test', testRoutes);
-const server = new ApolloServer({
- typeDefs: graphqlSchema,
- context: getGraphQLContext,
- resolvers,
- // The newer IDE does not work because of CORS issues
- plugins: [ApolloServerPluginLandingPageGraphQLPlayground()]
-});
-server.start().then(() => {
- server.applyMiddleware({ app });
+apolloServer.start().then(() => {
+ apolloServer.applyMiddleware({ app });
});
const listener = express();
diff --git a/server/apps/embed.js b/server/apps/embed.js
index 5226a83b7..7275a30ab 100644
--- a/server/apps/embed.js
+++ b/server/apps/embed.js
@@ -1,41 +1,39 @@
const express = require('express');
const { resolve } = require('path');
const { create } = require('express-handlebars');
-const {
- ApolloClient,
- gql,
- HttpLink,
- InMemoryCache
-} = require('@apollo/client');
-const fetch = require('cross-fetch');
+const { gql } = require('apollo-server-core');
+const apolloServer = require('../graphql-server');
+const staleWhileRevalidate = require('../util/staleWhileRevalidate');
+const hash = require('object-hash');
const app = express();
const handlebarsPath =
- process.env.ENVIRONMENT === 'dev' ? 'handlebars' : 'server/handlebars';
+ process.env.ENVIRONMENT === 'dev' || process.env.ENVIRONMENT === 'test'
+ ? 'handlebars'
+ : 'server/handlebars';
// handlebars
const hbs = create({
- layoutsDir: resolve(`${handlebarsPath}/views/layouts`),
+ layoutsDir: resolve(handlebarsPath, 'views/layouts'),
extname: 'hbs',
defaultLayout: 'index',
- helpers: require(resolve(`${handlebarsPath}/helpers`))
+ helpers: require(resolve(handlebarsPath, 'helpers'))
});
app.engine('hbs', hbs.engine);
app.set('view engine', 'hbs');
-app.set('views', resolve(`${handlebarsPath}/views`));
-
-if (process.env.ENVIRONMENT !== 'dev') {
- app.enable('view cache');
-}
-
-const client = new ApolloClient({
- link: new HttpLink({ uri: 'http://localhost:8000/api/graphql', fetch }),
- cache: new InMemoryCache()
-});
-
-const getLatestReportsForPattern = async pattern => {
- const { data } = await client.query({
+app.set('views', resolve(handlebarsPath, 'views'));
+
+// Prevent refreshing cached data for five seconds - using a short time like
+// this is possible because the stale-while-revalidate caching strategy works in
+// the background and doesn't spin up more than one simultaneous request.
+//
+// If queries are very slow, anyone trying to get the refreshed data will get
+// stale data for however long it takes for the query to complete.
+const millisecondsUntilStale = 5000;
+
+const queryReports = async () => {
+ const { data, errors } = await apolloServer.executeOperation({
query: gql`
query {
testPlanReports(statuses: [CANDIDATE, RECOMMENDED]) {
@@ -68,9 +66,26 @@ const getLatestReportsForPattern = async pattern => {
`
});
+ if (errors) {
+ throw new Error(errors);
+ }
+
+ const reportsHashed = hash(data.testPlanReports);
+
+ return { allTestPlanReports: data.testPlanReports, reportsHashed };
+};
+
+// As of now, a full query for the complete list of reports is needed to build
+// the embed for a single pattern. This caching allows that query to be reused
+// between pattern embeds.
+const queryReportsCached = staleWhileRevalidate(queryReports, {
+ millisecondsUntilStale
+});
+
+const getLatestReportsForPattern = ({ allTestPlanReports, pattern }) => {
let title;
- const testPlanReports = data.testPlanReports.filter(report => {
+ const testPlanReports = allTestPlanReports.filter(report => {
if (report.testPlanVersion.testPlan.id === pattern) {
title = report.testPlanVersion.title;
return true;
@@ -80,7 +95,6 @@ const getLatestReportsForPattern = async pattern => {
let allAts = new Set();
let allBrowsers = new Set();
let allAtVersionsByAt = {};
- let status = 'RECOMMENDED';
let reportsByAt = {};
let testPlanVersionIds = new Set();
const uniqueReports = [];
@@ -89,9 +103,6 @@ const getLatestReportsForPattern = async pattern => {
testPlanReports.forEach(report => {
allAts.add(report.at.name);
allBrowsers.add(report.browser.name);
- if (report.status === 'CANDIDATE') {
- status = report.status;
- }
if (!allAtVersionsByAt[report.at.name])
allAtVersionsByAt[report.at.name] =
@@ -155,6 +166,11 @@ const getLatestReportsForPattern = async pattern => {
.sort((a, b) => a.browser.name.localeCompare(b.browser.name));
});
+ const hasAnyCandidateReports = Object.values(reportsByAt).find(atReports =>
+ atReports.find(report => report.status === 'CANDIDATE')
+ );
+ let status = hasAnyCandidateReports ? 'CANDIDATE' : 'RECOMMENDED';
+
return {
title,
allBrowsers,
@@ -165,16 +181,13 @@ const getLatestReportsForPattern = async pattern => {
};
};
-app.get('/reports/:pattern', async (req, res) => {
- // In the instance where an editor doesn't want to display a certain title
- // as it has defined when importing into the ARIA-AT database for being too
- // verbose, etc. eg. `Link Example 1 (span element with text content)`
- // Usage: https://aria-at.w3.org/embed/reports/command-button?title=Link+Example+(span+element+with+text+content)
- const queryTitle = req.query.title;
- const pattern = req.params.pattern;
- const protocol = /dev|vagrant/.test(process.env.ENVIRONMENT)
- ? 'http://'
- : 'https://';
+const renderEmbed = ({
+ allTestPlanReports,
+ queryTitle,
+ pattern,
+ protocol,
+ host
+}) => {
const {
title,
allBrowsers,
@@ -182,8 +195,8 @@ app.get('/reports/:pattern', async (req, res) => {
testPlanVersionIds,
status,
reportsByAt
- } = await getLatestReportsForPattern(pattern);
- res.render('main', {
+ } = getLatestReportsForPattern({ pattern, allTestPlanReports });
+ return hbs.renderView(resolve(handlebarsPath, 'views/main.hbs'), {
layout: 'index',
dataEmpty: Object.keys(reportsByAt).length === 0,
title: queryTitle || title || 'Pattern Not Found',
@@ -192,11 +205,44 @@ app.get('/reports/:pattern', async (req, res) => {
allBrowsers,
allAtVersionsByAt,
reportsByAt,
- completeReportLink: `${protocol}${
- req.headers.host
- }/report/${testPlanVersionIds.join(',')}`,
- embedLink: `${protocol}${req.headers.host}/embed/reports/${pattern}`
+ completeReportLink: `${protocol}${host}/report/${testPlanVersionIds.join(
+ ','
+ )}`,
+ embedLink: `${protocol}${host}/embed/reports/${pattern}`
+ });
+};
+
+// Limit the number of times the template is rendered
+const renderEmbedCached = staleWhileRevalidate(renderEmbed, {
+ getCacheKeyFromArguments: ({ reportsHashed, pattern }) =>
+ reportsHashed + pattern,
+ millisecondsUntilStale
+});
+
+app.get('/reports/:pattern', async (req, res) => {
+ // In the instance where an editor doesn't want to display a certain title
+ // as it has defined when importing into the ARIA-AT database for being too
+ // verbose, etc. eg. `Link Example 1 (span element with text content)`
+ // Usage: https://aria-at.w3.org/embed/reports/command-button?title=Link+Example+(span+element+with+text+content)
+ const queryTitle = req.query.title;
+ const pattern = req.params.pattern;
+ const host = req.headers.host;
+ const protocol = /dev|vagrant/.test(process.env.ENVIRONMENT)
+ ? 'http://'
+ : 'https://';
+ const { allTestPlanReports, reportsHashed } = await queryReportsCached();
+ const embedRendered = await renderEmbedCached({
+ allTestPlanReports,
+ reportsHashed,
+ queryTitle,
+ pattern,
+ protocol,
+ host
});
+
+ // Disable browser-based caching which could potentially make the embed
+ // contents appear stale even after being refreshed
+ res.set('cache-control', 'must-revalidate').send(embedRendered);
});
app.use(express.static(resolve(`${handlebarsPath}/public`)));
diff --git a/server/graphql-context.js b/server/graphql-context.js
index a78f6bfc3..b12969db2 100644
--- a/server/graphql-context.js
+++ b/server/graphql-context.js
@@ -1,5 +1,6 @@
const getGraphQLContext = ({ req }) => {
- const user = req.session && req.session.user ? req.session.user : null;
+ const user =
+ req && req.session && req.session.user ? req.session.user : null;
return { user };
};
diff --git a/server/graphql-server.js b/server/graphql-server.js
new file mode 100644
index 000000000..f7e87a398
--- /dev/null
+++ b/server/graphql-server.js
@@ -0,0 +1,17 @@
+const { ApolloServer } = require('apollo-server-express');
+const {
+ ApolloServerPluginLandingPageGraphQLPlayground
+} = require('apollo-server-core');
+const graphqlSchema = require('./graphql-schema');
+const getGraphQLContext = require('./graphql-context');
+const resolvers = require('./resolvers');
+
+const apolloServer = new ApolloServer({
+ typeDefs: graphqlSchema,
+ context: getGraphQLContext,
+ resolvers,
+ // The newer IDE does not work because of CORS issues
+ plugins: [ApolloServerPluginLandingPageGraphQLPlayground()]
+});
+
+module.exports = apolloServer;
diff --git a/server/handlebars/public/script.js b/server/handlebars/public/script.js
index 5ab466b1e..e73f604c2 100644
--- a/server/handlebars/public/script.js
+++ b/server/handlebars/public/script.js
@@ -1,12 +1,12 @@
-const iframeClass = `support-levels-${document.currentScript.getAttribute(
- 'pattern'
-)}`;
+const iframeClass = `support-levels-${document
+ .querySelector('script[pattern]')
+ .getAttribute('pattern')}`;
const iframeCode = link =>
``;
diff --git a/server/handlebars/public/style.css b/server/handlebars/public/style.css
index dca8f2dce..113468f2d 100644
--- a/server/handlebars/public/style.css
+++ b/server/handlebars/public/style.css
@@ -71,8 +71,12 @@ h3#report-title {
display: inline-block;
}
-#candidate-title.recommended h3 {
- width: 130px;
+#candidate-title.recommended {
+ border: 1.5px solid #7ac498;
+ background-color: #e9fbe9;
+}
+#candidate-title.recommended > span {
+ background-color: #115b11;
}
#candidate-content-container {
@@ -90,7 +94,7 @@ h3#report-title {
margin-bottom: 3px;
}
-.none {
+.no-data-cell {
display: block;
color: #72777f;
font-style: italic;
@@ -187,16 +191,19 @@ table tbody tr td {
background-color: #f4f4f4;
}
-#view-report-button, #embed-button {
+#view-report-button,
+#embed-button {
margin-bottom: 1em;
padding: 0 12px;
line-height: 36px;
}
-#view-report-button:focus-visible, #embed-button:focus-visible {
+#view-report-button:focus-visible,
+#embed-button:focus-visible {
outline-offset: 2px;
outline: 2px solid #3a86d1;
}
-#view-report-button svg, #embed-button svg {
+#view-report-button svg,
+#embed-button svg {
width: 24px;
margin-right: 8px;
float: left;
diff --git a/server/handlebars/views/layouts/index.hbs b/server/handlebars/views/layouts/index.hbs
index 62c2b08fa..03f471d6a 100644
--- a/server/handlebars/views/layouts/index.hbs
+++ b/server/handlebars/views/layouts/index.hbs
@@ -2,7 +2,7 @@
-
+
ARIA-AT Report
@@ -10,7 +10,5 @@
{{{body}}}
-
-
\ No newline at end of file
+
diff --git a/server/handlebars/views/main.hbs b/server/handlebars/views/main.hbs
index e2b8292ea..50794f025 100644
--- a/server/handlebars/views/main.hbs
+++ b/server/handlebars/views/main.hbs
@@ -1,6 +1,5 @@
{{#if dataEmpty}}
-
{{title}}
There is no data for this pattern.
@@ -13,7 +12,7 @@
Warning! Unapproved Report
The information in this report is generated from candidate tests developed and run by the ARIA-AT Project.
- ARIA-AT tests are in review by assistive technology developers and lack consensus regarding:
+ Candidate ARIA-AT tests are in review by assistive technology developers and lack consensus regarding:
applicability and validity of the tests, and
accuracy of test results.
@@ -21,18 +20,17 @@
{{else}}
-
-
Recommended Report
+
+ Recommended Report
The information in this report is generated from recommended tests.
- Recommended ARIA-AT tests have been reviewed by assistive technology
- developers and represent consensus regarding
+ Recommended ARIA-AT tests have been reviewed by assistive technology developers and represent consensus regarding: