diff --git a/app/components/user-avatar.js b/app/components/user-avatar.js
index 75d97efad8e..9d68f93a65b 100644
--- a/app/components/user-avatar.js
+++ b/app/components/user-avatar.js
@@ -5,7 +5,7 @@ import { computed } from '@ember/object';
export default Component.extend({
size: 'small',
user: null,
- attributeBindings: ['src', 'width', 'height'],
+ attributeBindings: ['src', 'width', 'height', 'alt'],
tagName: 'img',
width: computed('size', function() {
@@ -20,6 +20,10 @@ export default Component.extend({
height: readOnly('width'),
+ alt: computed('user', function() {
+ return `${this.get('user.name')} (${this.get('user.login')})`;
+ }),
+
src: computed('size', 'user', function() {
return `${this.get('user.avatar')}&s=${this.get('width') * 2}`;
})
diff --git a/app/styles/app.scss b/app/styles/app.scss
index d21c08f0ae3..77167e1a195 100644
--- a/app/styles/app.scss
+++ b/app/styles/app.scss
@@ -84,7 +84,6 @@ body {
button {
background: none;
- outline: 0;
border: 0;
padding: 10px 0;
@@ -130,7 +129,6 @@ body {
font-size: 90%;
border: none;
color: black;
- outline: 0;
margin-left: 30px;
padding: 5px 5px 5px 25px;
background-color: white;
@@ -141,6 +139,15 @@ body {
@include border-radius(15px);
}
+form.search label {
+ position: absolute;
+ left: -10000px;
+ top: auto;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+}
+
#header input.search {
width: 100%;
margin-left: 16px;
diff --git a/app/styles/crate.scss b/app/styles/crate.scss
index 44e2f092b74..21c6a54a36c 100644
--- a/app/styles/crate.scss
+++ b/app/styles/crate.scss
@@ -190,6 +190,12 @@
font-size: 90%;
margin-bottom: 20px;
+ ol {
+ list-style: none;
+ padding: 0;
+ }
+ ol, li { display: inline; }
+
a {
color: $main-color-light;
text-decoration: none;
diff --git a/app/styles/home.scss b/app/styles/home.scss
index 09fb5f6d242..e8316b36979 100644
--- a/app/styles/home.scss
+++ b/app/styles/home.scss
@@ -20,7 +20,7 @@
margin-right: 10px;
}
- &:hover { @include vertical-gradient($start_dark, $end_dark); outline: 0; }
+ &:hover, &:focus { @include vertical-gradient($start_dark, $end_dark); outline: 0; }
&.active { @include vertical-gradient($start_dark, $end_dark); outline: 0; }
&[disabled] {
@include vertical-gradient($start_light, $end_light);
@@ -111,9 +111,17 @@ button.small {
line-height: 20px;
}
- > div { margin: 0 15px; }
+ > section { margin: 0 15px; }
- ul { list-style: none; padding: 0; }
+ > section[aria-busy="true"] > h2::after {
+ content: '';
+ background-image: url('/assets/ajax-loader.gif');
+ display: inline-block;
+ height: 16px;
+ width: 16px;
+ }
+
+ ul, ol { list-style: none; padding: 0; }
li { margin: 8px 0; }
li a {
@@ -143,7 +151,7 @@ button.small {
@include flex-wrap(wrap);
@include justify-content(left);
- > div {
+ > section {
margin: 0;
padding: 0 15px;
width: 33.33%;
diff --git a/app/styles/me.scss b/app/styles/me.scss
index 07a8068f4ad..0f09ce629a6 100644
--- a/app/styles/me.scss
+++ b/app/styles/me.scss
@@ -177,7 +177,7 @@
margin-bottom: 20px;
background-color: $bg;
color: white;
- &:hover { background-color: darken($bg, 10%); }
+ &:hover, &:focus { background-color: darken($bg, 10%); }
}
}
diff --git a/app/templates/application.hbs b/app/templates/application.hbs
index 8e60329cfdc..0979f8285fc 100644
--- a/app/templates/application.hbs
+++ b/app/templates/application.hbs
@@ -1,12 +1,8 @@
{{title "Cargo: packages for Rust" separator=' - ' prepend=true}}
{{google-jsapi}}
-
-
-
-
-
-
-
New Crates {{#if dataTask.isRunning}}
{{/if}}
+
+ New Crates
{{crate-list crates=model.new_crates}}
-
-
-
Most Downloaded {{#if dataTask.isRunning}}
{{/if}}
+
+
+ Most Downloaded
{{crate-list crates=model.most_downloaded}}
-
-
-
Just Updated {{#if dataTask.isRunning}}
{{/if}}
+
+
+ Just Updated
{{crate-list crates=model.just_updated}}
-
-
-
Most Recent Downloads {{#if dataTask.isRunning}}
{{/if}}
+
+
+ Most Recent Downloads
{{crate-list crates=model.most_recently_downloaded}}
-
-
-
Popular Keywords {{#link-to 'keywords'}}(see all){{/link-to}} {{#if dataTask.isRunning}}
{{/if}}
+
+
+ Popular Keywords {{#link-to 'keywords'}}(see all){{/link-to}}
{{keyword-list keywords=model.popular_keywords}}
-
-
-
Popular Categories {{#link-to 'categories'}}(see all){{/link-to}} {{#if dataTask.isRunning}}
{{/if}}
+
+
+ Popular Categories {{#link-to 'categories'}}(see all){{/link-to}}
{{category-list categories=model.popular_categories}}
-
+
diff --git a/config/environment.js b/config/environment.js
index e30fa3535a7..66948c54bc8 100644
--- a/config/environment.js
+++ b/config/environment.js
@@ -39,6 +39,11 @@ module.exports = function(environment) {
// ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
// ENV.APP.LOG_VIEW_LOOKUPS = true;
+ ENV['ember-a11y-testing'] = {
+ componentOptions: {
+ turnAuditOff: true,
+ }
+ };
}
if (environment === 'test') {
diff --git a/package-lock.json b/package-lock.json
index b70495ad1c2..c5d5c79aaa6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -454,6 +454,12 @@
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=",
"dev": true
},
+ "axe-core": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-2.6.1.tgz",
+ "integrity": "sha512-QFfI3d+x/v92HJWGBaNfgrxdfon9/xXzd04YYfm5w5NJQOLex8qkJCctOt7+ky6e1l9zcQ5E7jsvbnTgQzyfTw==",
+ "dev": true
+ },
"babel-code-frame": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz",
@@ -3151,6 +3157,51 @@
"integrity": "sha1-qWfr3P6O0Ag/wkTRiUAiqOgRPqI=",
"dev": true
},
+ "ember-a11y-testing": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/ember-a11y-testing/-/ember-a11y-testing-0.5.0.tgz",
+ "integrity": "sha512-GgUqyY6hFIzyTly4rvezr5FoSjOshLcbLPu+vgBkhRHOehD18jAWtHVosOnBRby7tbxobp28DbU9083rz65bQg==",
+ "dev": true,
+ "requires": {
+ "axe-core": "2.6.1",
+ "broccoli-funnel": "2.0.1",
+ "ember-cli-babel": "6.8.2",
+ "ember-cli-version-checker": "2.1.0"
+ },
+ "dependencies": {
+ "broccoli-funnel": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/broccoli-funnel/-/broccoli-funnel-2.0.1.tgz",
+ "integrity": "sha512-C8Lnp9TVsSSiZMGEF16C0dCiNg2oJqUKwuZ1K4kVC6qRPG/2Cj/rtB5kRCC9qEbwqhX71bDbfHROx0L3J7zXQg==",
+ "dev": true,
+ "requires": {
+ "array-equal": "1.0.0",
+ "blank-object": "1.0.2",
+ "broccoli-plugin": "1.3.0",
+ "debug": "2.6.8",
+ "fast-ordered-set": "1.0.3",
+ "fs-tree-diff": "0.5.6",
+ "heimdalljs": "0.2.5",
+ "minimatch": "3.0.4",
+ "mkdirp": "0.5.1",
+ "path-posix": "1.0.0",
+ "rimraf": "2.6.1",
+ "symlink-or-copy": "1.1.8",
+ "walk-sync": "0.3.2"
+ }
+ },
+ "ember-cli-version-checker": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ember-cli-version-checker/-/ember-cli-version-checker-2.1.0.tgz",
+ "integrity": "sha512-ssiNyVTp+PphroFum8guHX9py4xU1PCxkRYgb25NxumgjpKTPjhkgTfpRRKXlIQe+/wVMmhf+Uv6w9vSLZKWKQ==",
+ "dev": true,
+ "requires": {
+ "resolve": "1.3.3",
+ "semver": "5.3.0"
+ }
+ }
+ }
+ },
"ember-ajax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ember-ajax/-/ember-ajax-3.0.0.tgz",
diff --git a/package.json b/package.json
index bfa35cfafe3..5c810d87382 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"devDependencies": {
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"broccoli-asset-rev": "2.5.0",
+ "ember-a11y-testing": "^0.5.0",
"ember-ajax": "^3.0.0",
"ember-cli": "~2.17.0",
"ember-cli-app-version": "^3.0.0",
diff --git a/tests/acceptance/categories-test.js b/tests/acceptance/categories-test.js
index 4a819c19814..ad423603575 100644
--- a/tests/acceptance/categories-test.js
+++ b/tests/acceptance/categories-test.js
@@ -1,9 +1,31 @@
import { test } from 'qunit';
import { visit } from 'ember-native-dom-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
+import axeConfig from '../axe-config';
moduleForAcceptance('Acceptance | categories');
+test('is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.create('category', { category: 'API bindings', crates_cnt: 0 });
+ server.create('category', { category: 'Algorithms', crates_cnt: 1 });
+ server.create('category', { category: 'Asynchronous', crates_cnt: 3910 });
+
+ await visit('/categories');
+ await a11yAudit(axeConfig);
+});
+
+test('category/:category_id is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.create('category', { category: 'Algorithms', crates_cnt: 1 });
+
+ await visit('/categories/algorithms');
+ await a11yAudit(axeConfig);
+});
+
test('listing categories', async function(assert) {
server.create('category', { category: 'API bindings', crates_cnt: 0 });
server.create('category', { category: 'Algorithms', crates_cnt: 1 });
diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js
index 0b374514070..a496ed0d9ba 100644
--- a/tests/acceptance/crate-test.js
+++ b/tests/acceptance/crate-test.js
@@ -1,10 +1,52 @@
import Service from '@ember/service';
import { test } from 'qunit';
import { click, visit, currentURL, currentRouteName, fillIn } from 'ember-native-dom-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
+import axeConfig from '../axe-config';
moduleForAcceptance('Acceptance | crate page');
+test('is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.create('crate', 'withVersion', { id: 'nanomsg' });
+
+ await visit('/');
+ await a11yAudit(axeConfig);
+});
+
+test('/crates/:crate is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.create('crate', { id: 'nanomsg', max_version: '0.6.1' });
+ server.create('version', { crate: 'nanomsg', num: '0.6.0' });
+ server.create('version', { crate: 'nanomsg', num: '0.6.1' });
+
+ await visit('/crates/nanomsg');
+ await a11yAudit(axeConfig);
+});
+
+test('/crates/:crate/:version is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.create('crate', { id: 'nanomsg', max_version: '0.6.1' });
+ server.create('version', { crate: 'nanomsg', num: '0.6.0' });
+ server.create('version', { crate: 'nanomsg', num: '0.6.1' });
+
+ await visit('/crates/nanomsg/0.6.0');
+ await a11yAudit(axeConfig);
+});
+
+test('/crates/:crate/owners is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.loadFixtures();
+
+ await visit('/crates/nanomsg/owners');
+ await a11yAudit(axeConfig);
+});
+
test('visiting a crate page from the front page', async function(assert) {
server.create('crate', 'withVersion', { id: 'nanomsg' });
diff --git a/tests/acceptance/crates-test.js b/tests/acceptance/crates-test.js
index 8af3f6c3927..f53faf12889 100644
--- a/tests/acceptance/crates-test.js
+++ b/tests/acceptance/crates-test.js
@@ -1,9 +1,29 @@
import { test } from 'qunit';
import { click, visit, currentURL } from 'ember-native-dom-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
+import axeConfig from '../axe-config';
moduleForAcceptance('Acceptance | crates page');
+test('is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.loadFixtures();
+
+ await visit('/');
+ await a11yAudit(axeConfig);
+});
+
+test('/crates is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.loadFixtures();
+
+ await visit('/crates');
+ await a11yAudit(axeConfig);
+});
+
test('visiting the crates page from the front page', async function(assert) {
server.loadFixtures();
diff --git a/tests/acceptance/front-page-test.js b/tests/acceptance/front-page-test.js
index d14cb0d5072..0d585819829 100644
--- a/tests/acceptance/front-page-test.js
+++ b/tests/acceptance/front-page-test.js
@@ -1,9 +1,20 @@
import { test } from 'qunit';
import { visit, currentURL } from 'ember-native-dom-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
+import axeConfig from '../axe-config';
moduleForAcceptance('Acceptance | front page');
+test('is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.loadFixtures();
+
+ await visit('/');
+ await a11yAudit(axeConfig);
+});
+
test('visiting /', async function(assert) {
server.loadFixtures();
diff --git a/tests/acceptance/keyword-test.js b/tests/acceptance/keyword-test.js
index 0531c59c53d..12bcd7e73ff 100644
--- a/tests/acceptance/keyword-test.js
+++ b/tests/acceptance/keyword-test.js
@@ -1,9 +1,20 @@
import { test } from 'qunit';
import { visit } from 'ember-native-dom-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
+import axeConfig from '../axe-config';
moduleForAcceptance('Acceptance | keywords');
+test('keyword/:keyword_id is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.create('keyword', { id: 'network', keyword: 'network', crates_cnt: 38 });
+
+ await visit('keywords/network');
+ await a11yAudit(axeConfig);
+});
+
test('keyword/:keyword_id index default sort is recent-downloads', async function(assert) {
server.create('keyword', { id: 'network', keyword: 'network', crates_cnt: 38 });
diff --git a/tests/acceptance/search-test.js b/tests/acceptance/search-test.js
index f8afb744abb..9d53eaed15f 100644
--- a/tests/acceptance/search-test.js
+++ b/tests/acceptance/search-test.js
@@ -1,10 +1,21 @@
import { test } from 'qunit';
import { fillIn, visit, triggerEvent, currentURL, blur } from 'ember-native-dom-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
import { triggerKeyDown, triggerKeyPress } from 'ember-keyboard';
+import axeConfig from '../axe-config';
moduleForAcceptance('Acceptance | search');
+test('/search?q=rust is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.loadFixtures();
+
+ await visit('/');
+ await a11yAudit(axeConfig);
+});
+
test('searching for "rust"', async function(assert) {
server.loadFixtures();
diff --git a/tests/acceptance/team-page-test.js b/tests/acceptance/team-page-test.js
index f17f0246ecb..d2a9f12a616 100644
--- a/tests/acceptance/team-page-test.js
+++ b/tests/acceptance/team-page-test.js
@@ -1,9 +1,20 @@
import { test } from 'qunit';
import { visit } from 'ember-native-dom-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
+import axeConfig from '../axe-config';
moduleForAcceptance('Acceptance | team page');
+test('is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.loadFixtures();
+
+ await visit('/teams/github:org:thehydroimpulse');
+ await a11yAudit(axeConfig);
+});
+
test('has team organization display', async function(assert) {
server.loadFixtures();
diff --git a/tests/acceptance/user-page-test.js b/tests/acceptance/user-page-test.js
index 516fca94654..c66717873b0 100644
--- a/tests/acceptance/user-page-test.js
+++ b/tests/acceptance/user-page-test.js
@@ -1,9 +1,20 @@
import { test } from 'qunit';
import { visit } from 'ember-native-dom-helpers';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
+import axeConfig from '../axe-config';
moduleForAcceptance('Acceptance | user page');
+test('is accessible', async function(assert) {
+ assert.expect(0);
+
+ server.loadFixtures();
+
+ await visit('/users/thehydroimpulse');
+ await a11yAudit(axeConfig);
+});
+
test('has user display', async function(assert) {
server.loadFixtures();
diff --git a/tests/axe-config.js b/tests/axe-config.js
new file mode 100644
index 00000000000..5231fe0e897
--- /dev/null
+++ b/tests/axe-config.js
@@ -0,0 +1,7 @@
+export default {
+ rules: {
+ 'color-contrast': {
+ enabled: false,
+ }
+ }
+};