diff --git a/.editorconfig b/.editorconfig index c2cdfb8a..a39be81e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,6 +11,9 @@ root = true indent_style = space indent_size = 2 +[*.json] +indent_size = 4 + # We recommend you to keep these unchanged end_of_line = lf charset = utf-8 diff --git a/app/images/Thumbs.db b/app/images/Thumbs.db deleted file mode 100644 index b33ba1d8..00000000 Binary files a/app/images/Thumbs.db and /dev/null differ diff --git a/app/images/w3c.png b/app/images/w3c.png deleted file mode 100644 index 5d73d1e2..00000000 Binary files a/app/images/w3c.png and /dev/null differ diff --git a/app/images/w3c.svg b/app/images/w3c.svg new file mode 100644 index 00000000..b26d95e0 --- /dev/null +++ b/app/images/w3c.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/wai.png b/app/images/wai.png deleted file mode 100644 index 0c6be961..00000000 Binary files a/app/images/wai.png and /dev/null differ diff --git a/app/images/wai.svg b/app/images/wai.svg new file mode 100644 index 00000000..c9fabf5e --- /dev/null +++ b/app/images/wai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/index.html b/app/index.html index e72d30eb..c258f828 100644 --- a/app/index.html +++ b/app/index.html @@ -49,12 +49,16 @@ + + + + @@ -109,6 +113,7 @@ + diff --git a/app/locale/EN/audit.json b/app/locale/EN/audit.json index 5bc34475..d18d23ed 100644 --- a/app/locale/EN/audit.json +++ b/app/locale/EN/audit.json @@ -1,31 +1,32 @@ { - "TITLE": "Step 4: Audit the Selected Sample", - "INTRO": "Record the outcome from evaluating the web pages selected in the previous step. Compare the results between the structured page and randomly selected pages, and if needed, adjust the selected sample in the previous step. More guidance on this step is provided in WCAG-EM Step 4: Audit the Selected Sample.
Note: For each WCAG 2.0 success criteria, you can enter 'Results for the entire sample' and you can enter results for individual web pages. You can choose to enter either or both. To enter individual results, select the web page(s) under 'Sample to Evaluate' (in the left column); then under the specific success criteria, select 'Show web pages to enter individual results'", - "INTRO_0": "", - "HD_SAMPLE_SELECT": "Sample to Evaluate", - "INF_AUDIT_SAMPLE": "This section lists the web pages you selected in the previous step. The web pages that are checked in this section are listed when 'Show web pages to enter individual results' is activated in the 'Success Criteria to Evaluate' section. You can show an individual web page that you are evaluating, or show several web pages at the same time.
The chainlink icon opens the web page in a separate browser window.", + "BTN_COLLAPSE_PAGES": "Hide web pages to enter individual results", "BTN_COMPLETE_SELECTED": "Set selected to complete", - "BTN_UNCOMPLETE_SELECTED": "Set selected to incomplete", + "BTN_EXPAND_PAGES": "Show web pages to enter individual results", "BTN_OPEN_SELECTED": "Open selected pages", - "NO_SAMPLE": "No sample available. Create a sample in step 2 and step 3.", - "TESTED": "Tested", - "HD_CRITERIA": "Success Criteria to Evaluate", - "INF_AUDIT_CRITERIA": "This section lists the WCAG 2.0 success criteria. Use the filter to show or hide success criteria of different levels (A, AA and AAA). You can select results as: 'Not checked', 'Passed', 'Failed', 'Not present', and 'Cannot tell'; and you can provide details, comments, or other observations made during evaluation in the accompanying text box.", - "FILTER": "Filter", - "PRINCIPLE": "Principle", - "NOTE": "Note", "BTN_SHOW_TEXT": "Show criterion text", - "UNDERSTAND": "Understanding", + "BTN_UNCOMPLETE_SELECTED": "Set selected to incomplete", + "CLICK_TO_DELETE": "Click to delete", + "FILTER": "Show", + "FILTER_NEW_IN_WCAG21": "Added in WCAG 2.1", + "HD_CRITERIA": "Success Criteria to Evaluate", + "HD_SAMPLE_SELECT": "Sample to Evaluate", "HOW_TO": "How to meet", - "SAMPLE_FINDINGS": "Results for the entire sample", - "BTN_EXPAND_PAGES": "Show web pages to enter individual results", - "BTN_COLLAPSE_PAGES": "Hide web pages to enter individual results", + "INF_AUDIT_CRITERIA": "This section lists the WCAG 2 success criteria. Use the filter to show or hide success criteria of different levels (A, AA and AAA). You can select results as: 'Not checked', 'Passed', 'Failed', 'Not present', and 'Cannot tell'; and you can provide details, comments, or other observations made during evaluation in the accompanying text box.", + "INF_AUDIT_SAMPLE": "This section lists the web pages you selected in the previous step. The web pages that are checked in this section are listed when 'Show web pages to enter individual results' is activated in the 'Success Criteria to Evaluate' section. You can show an individual web page that you are evaluating, or show several web pages at the same time.
The chainlink icon opens the web page in a separate browser window.", + "INTRO_0": "", + "INTRO": "Record the outcome from evaluating the web pages selected in the previous step. Compare the results between the structured page and randomly selected pages, and if needed, adjust the selected sample in the previous step. More guidance on this step is provided in WCAG-EM Step 4: Audit the Selected Sample.
Note: For each WCAG 2 success criteria, you can enter 'Results for the entire sample' and you can enter results for individual web pages. You can choose to enter either or both. To enter individual results, select the web page(s) under 'Sample to Evaluate' (in the left column); then under the specific success criteria, select 'Show web pages to enter individual results'", "LABEL_OUTCOME": "Outcome", - "UNTESTED": "{{critCount}} untested", + "LABEL_PAGE_HANDLE": "Short page name", "NO_PAGE_SELECTED": "No pages selected under Sample to Evaluate", + "NO_SAMPLE": "No sample available. Create a sample in step 2 and step 3.", + "NOTE": "Note", "PLH_ASSERT_DESC": "Observations made during evaluation", - "CLICK_TO_DELETE": "Click to delete", - "LABEL_PAGE_HANDLE": "Short page name", + "PRINCIPLE": "Principle", + "RESULTS_FOR": "Results for", + "SAMPLE_FINDINGS": "Results for the entire sample", "SELECT_ALL": "Select all web pages", - "RESULTS_FOR": "Results for" -} \ No newline at end of file + "TESTED": "Tested", + "TITLE": "Step 4: Audit the Selected Sample", + "UNDERSTAND": "Understanding", + "UNTESTED": "{{critCount}} untested" +} diff --git a/app/locale/EN/explore.json b/app/locale/EN/explore.json index e9cc54d5..7e259a1e 100644 --- a/app/locale/EN/explore.json +++ b/app/locale/EN/explore.json @@ -2,7 +2,7 @@ "TITLE": "Step 2: Explore the Target Website", "INTRO": "Explore the website to understand its purpose, functionality, and use. This step helps you determine which web pages to use for the evaluation in the next steps. Identify the web technologies used to provide the website and, if you want, take notes on other aspects of the website. Usually it is best to get input from the website owners and developers for this step. More guidance on this step is provided in WCAG-EM Step 2: Explore the Target Website.", "HD_RELIEDUP_TECH": "Web Technologies Relied Upon", - "INF_RELIEDUP_TECH": "Identify the web technologies relied upon according to WCAG 2.0 to provide the website. For more information, see WCAG-EM Step 2.d: Identify Web Technologies Relied Upon.
Note: To add other technologies, select 'Others' and use the 'Web Technology' and 'Specification Address (URL)' field. The 'Specification Address (URL)' field should identify the web technology specification.", + "INF_RELIEDUP_TECH": "Identify the web technologies relied upon according to WCAG 2 to provide the website. For more information, see WCAG-EM Step 2.d: Identify Web Technologies Relied Upon.
Note: To add other technologies, select 'Others' and use the 'Web Technology' and 'Specification Address (URL)' field. The 'Specification Address (URL)' field should identify the web technology specification.", "LABEL_TECH": "Web Technology", "LABEL_TECH_SPEC": "Specification Address (URL)", "PLH_TECH": "E.g. HTML5, CSS, DOM", @@ -16,4 +16,4 @@ "LABEL_VARIETY_PAGE_TYPES": "Variety of web page types", "INF_VARIETY_PAGE_TYPES": "You can use this field to take notes about the types (as opposed to instances) of web pages that you find on the web site. This includes notes about different styles, layouts, structures, and functionality provided on the website. For more information, see WCAG-EM Step 2.c: Identify the Variety of Web Page Types.
Note: 'Web pages' include different 'web page states'; see definition of web page states.", "LABEL_OTHER": "Others..." -} \ No newline at end of file +} diff --git a/app/locale/EN/import.json b/app/locale/EN/import.json new file mode 100644 index 00000000..db4dc110 --- /dev/null +++ b/app/locale/EN/import.json @@ -0,0 +1,5 @@ +{ + "TITLE": "Import a WCAG EARL report", + "INTRO": "If you have used another application to generate a WCAG EARL-report that falls into the scope of this evaluation, you may be able to import these and add them to the evaluation audit result. The file itself should be: JSON-LD parseable, consist of objects in EARL format and evaluation tests should be related to WCAG.", + "LABEL_SELECT_FILE": "Select a JSON-LD file" +} diff --git a/app/locale/EN/nav.json b/app/locale/EN/nav.json index a4600736..9f436e74 100644 --- a/app/locale/EN/nav.json +++ b/app/locale/EN/nav.json @@ -1,5 +1,7 @@ -{ +{ "MENU": "Menu", + "MENU_IMPORT": "Import", + "MENU_IMPORT_TITLE": "Start EARL report import wizzard", "MENU_NEW": "New Report", "MENU_OPEN": "Open", "MENU_SAVE": "Save", @@ -25,4 +27,4 @@ "STEP_REPORT": "Report Findings", "STEP_VIEWREPORT": "View Report", "BTN_BACK_TO_EVAL": "Back" -} \ No newline at end of file +} diff --git a/app/locale/EN/scope.json b/app/locale/EN/scope.json index b19b7134..dc55b120 100644 --- a/app/locale/EN/scope.json +++ b/app/locale/EN/scope.json @@ -9,6 +9,10 @@ "INF_SITE_SCOPE_LI1": "'All web content of the online shop of Example Org. located at http://www.example.org/shop/'", "INF_SITE_SCOPE_LI2": "'All web content of the mobile version of the public website of Example Org. located at http://m.example.org'", "INF_SITE_SCOPE_1": "WCAG-EM Step 1.a: Define the Scope of the Website", + "LABEL_WCAG_VERSION": "WCAG Version", + "INFO_WCAG_VERSION": "Select the WCAG version to use. Version 2.1 (default) or 2.0", + "WCAG21": "WCAG 2.1", + "WCAG20": "WCAG 2.0", "LABEL_CONFORMANCE_TGT": "Conformance target", "INF_CONF_TGT": "Select a target WCAG 2 conformance level ('A', 'AA', or 'AAA') for the evaluation. For more information, see WCAG-EM Step 1.b: Define the Conformance Target. This selection determines which conformance level filters are active by default in 'step 4: Audit the Sample'.", "LABEL_SUPPORT_BASE": "Accessibility support baseline", diff --git a/app/locale/EN/start.json b/app/locale/EN/start.json index 61e3f359..eccc46b9 100644 --- a/app/locale/EN/start.json +++ b/app/locale/EN/start.json @@ -2,7 +2,7 @@ "TITLE": "WCAG-EM Report Tool", "SUBTITLE": "Website Accessibility Evaluation Report Generator", "INTRO_HD": "What this tool does", - "INTRO_1": "This tool helps you generate a report according to the Website Accessibility Conformance Evaluation Methodology (WCAG-EM). It does not perform any accessibility checks. It helps you follow the steps of WCAG-EM, to generate a structured report from the input that you provide. It is designed for experienced evaluators who know Web Content Accessibility Guidelines (WCAG) 2.0 and are somewhat familiar with WCAG-EM. For an introduction to WCAG-EM, see the WCAG-EM Overview.", + "INTRO_1": "This tool helps you generate a report according to the Website Accessibility Conformance Evaluation Methodology (WCAG-EM). It does not perform any accessibility checks. It helps you follow the steps of WCAG-EM, to generate a structured report from the input that you provide. It is designed for experienced evaluators who know Web Content Accessibility Guidelines (WCAG) 2 and are somewhat familiar with WCAG-EM. For an introduction to WCAG-EM, see the WCAG-EM Overview.", "INTRO_2": "Note: This tool does not automatically save the information that you enter. To save your data in a file locally on your computer, use Windows shortcut keys Ctrl+S or Mac shortcut keys {{mac}} to open the Save dialog. (Or the 'Save' link at the top of the page will open the Save Evaluation Report page and from there the 'Save data file locally to your computer' link will open the Save dialog.)", "USAGE_HD": "How this tool works", "USAGE_LI1": "All functionality provided by this tool is now loaded and running locally in your web browser. You don't need an Internet connection beyond this point. When you close your web browser window, any unsaved data is lost.", @@ -13,5 +13,5 @@ "TIPS_LI1": "You can go back and forth between the steps in any order. None of the fields are required.", "TIPS_LI2": "To get more information about a field, select the {{info}} icon next to the field label.", "TIPS_LI3": "The tool provides your report as HTML and CSS files. You can download these files from the 'View Report' page. You can then change the report content and visual design.", - "TIPS_LI4": "You can include in your report WCAG 2.0 success criteria beyond the conformance target. For example, the website is only required to meet Level AA, yet you want to also include Level AAA success criteria in the report. In step 4, use the level filter to show higher level criteria. Any criteria with a result will always be included in the report." -} \ No newline at end of file + "TIPS_LI4": "You can include in your report WCAG 2 success criteria beyond the conformance target. For example, the website is only required to meet Level AA, yet you want to also include Level AAA success criteria in the report. In step 4, use the level filter to show higher level criteria. Any criteria with a result will always be included in the report." +} diff --git a/app/scripts/app.js b/app/scripts/app.js index 24e53aae..99c3c270 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -11,10 +11,11 @@ angular.module('wcagReporter', [ $compileProvider .aHrefSanitizationWhitelist(/^\s*(https?|data|blob):/); - $routeProvider.when('/', { - templateUrl: 'views/start.html', - controller: 'StartCtrl' - }) + $routeProvider + .when('/', { + templateUrl: 'views/start.html', + controller: 'StartCtrl' + }) .when('/evaluation/scope', { templateUrl: 'views/evaluation/scope.html', controller: 'EvalScopeCtrl' @@ -50,6 +51,10 @@ angular.module('wcagReporter', [ .when('/error', { templateUrl: 'views/error.html' }) + .when('/import', { + templateUrl: 'views/import.html', + controller: 'ImportCtrl' + }) .otherwise({ redirectTo: '/error' }); diff --git a/app/scripts/controllers/evaluation/audit/criteria.js b/app/scripts/controllers/evaluation/audit/criteria.js index 4881dc4b..53c9e7e0 100644 --- a/app/scripts/controllers/evaluation/audit/criteria.js +++ b/app/scripts/controllers/evaluation/audit/criteria.js @@ -13,6 +13,7 @@ angular.module('wcagReporter') $timeout ) { var principlesOrigin = []; + var activeFilters = []; $scope.criteria = evalAuditModel.getCriteriaSorted(); @@ -22,10 +23,6 @@ angular.module('wcagReporter') var tgtPrinciple = origin[target.length]; target.push(tgtPrinciple); - // tgtPrinciple.guidelines.forEach(function (g) { - // g.hideCriteria = true; - // }); - if (target.length !== origin.length) { $timeout(function () { buildPrinciples(target, origin); @@ -33,6 +30,84 @@ angular.module('wcagReporter') } } + // Read from critFilter + function getActiveFilters () { + var filters = $scope.critFilter; + var activatedFilters = []; + + for (var filter in filters) { + // levels is an object with level key value boolean + if ( + Object.prototype.hasOwnProperty.call($scope.critFilter, filter) && + typeof filters[filter] === 'object' + ) { + for (var filterOption in filters[filter]) { + if ( + Object.prototype.hasOwnProperty.call(filters[filter], filterOption) && + filters[filter][filterOption] === true + ) { + activatedFilters.push(filterOption); + } + } + } + + // version is a string; WCAG21, WCAG20 or WCAG20 WCAG21 + if ( + Object.prototype.hasOwnProperty.call($scope.critFilter, filter) && + typeof filters[filter] === 'string' + ) { + filters[filter].split(' ') + .forEach(function (filterOption) { + activatedFilters.push(filterOption); + }); + } + } + + return activatedFilters.slice(); + } + + function setActiveFilters () { + activeFilters = getActiveFilters(); + } + + function filteredByLevel () { + var levelFilter = $scope.critFilter.levels; + + for (var level in levelFilter) { + if ( + Object.prototype.hasOwnProperty.call(levelFilter, level) && + levelFilter[level] === true + ) { + return true; + } + } + + return false; + } + + function criterionMatchFilter (criterion) { + var versionActive = (activeFilters.indexOf(criterion.versions[0]) !== -1); + var levelActive = (activeFilters.indexOf(criterion.level) !== -1); + + if ( + versionActive && + levelActive + ) { + return true; + } + + // Version filtering is always on so if no level is filtered + // show criteria based on version occurence alone + if ( + versionActive && + !filteredByLevel() + ) { + return true; + } + + return false; + } + $scope.principles = []; if (wcag2spec.isLoaded()) { @@ -46,54 +121,47 @@ angular.module('wcagReporter') buildPrinciples($scope.principles, principlesOrigin); }); + $scope.handleFilterChange = function handleFilterChange () { + setActiveFilters(); + }; + if ($rootScope.rootHide.criteria) { - $scope.critFilter = $rootScope.rootHide.criteria; + $scope.critFilter = $rootScope.rootHide.criteria; } else { - $scope.critFilter = { - 'wai:WCAG2A-Conformance': evalScopeModel.matchConformTarget('wai:WCAG2A-Conformance'), - 'wai:WCAG2AA-Conformance': evalScopeModel.matchConformTarget('wai:WCAG2AA-Conformance'), - 'wai:WCAG2AAA-Conformance': evalScopeModel.matchConformTarget('wai:WCAG2AAA-Conformance') + $scope.critFilter = { + version: evalScopeModel.wcagVersion === 'WCAG21' + ? 'WCAG21 WCAG20' + : 'WCAG20', + levels: { + 'wai:WCAG2A-Conformance': evalScopeModel.matchConformTarget('wai:WCAG2A-Conformance'), + 'wai:WCAG2AA-Conformance': evalScopeModel.matchConformTarget('wai:WCAG2AA-Conformance'), + 'wai:WCAG2AAA-Conformance': evalScopeModel.matchConformTarget('wai:WCAG2AAA-Conformance') + } }; - $rootScope.rootHide.criteria = $scope.critFilter; + + $rootScope.rootHide.criteria = $scope.critFilter; } + setActiveFilters(); $scope.isCriterionVisible = function (critSpec) { // Check if the level of this criterion should be shown - if ($scope.critFilter[critSpec.level] !== true) { - return false; - } - - // Check if the assert has an outcome, if no, don't show the criterion - var critAssert = evalAuditModel.getCritAssert(critSpec.id); - if (typeof critAssert !== 'object' || - typeof critAssert.result !== 'object') { + if (!criterionMatchFilter(critSpec)) { return false; } - // Check if the outcome is set to hidden - return true; + return true; }; $scope.isGuidelineVisible = function (guideline) { - var visible = false; - guideline.successcriteria.forEach(function (critSpec) { - // Only check the criterion if a previous check hasn't already returned true - if (visible || $scope.isCriterionVisible(critSpec)) { - visible = true; - } - }); - return visible; + return guideline.successcriteria.some(function (criterion) { + return $scope.isCriterionVisible(criterion); + }); }; $scope.isPrincipleVisible = function (principle) { - var visible = false; - principle.guidelines.forEach(function (guideline) { - // Only check the criterion if a previous check hasn't already returned true - if (visible || $scope.isGuidelineVisible(guideline)) { - visible = true; - } + return principle.guidelines.some(function (guideline) { + return $scope.isGuidelineVisible(guideline); }); - return visible; }; var untested = [ diff --git a/app/scripts/controllers/evaluation/explore.js b/app/scripts/controllers/evaluation/explore.js index 20487f12..712365db 100644 --- a/app/scripts/controllers/evaluation/explore.js +++ b/app/scripts/controllers/evaluation/explore.js @@ -1,24 +1,31 @@ 'use strict'; -angular.module('wcagReporter') - .controller( - 'EvalExploreCtrl', - function ($scope, appState, $timeout, evalExploreModel) { - $scope.state = appState.moveToState('explore'); - $scope.exploreModel = evalExploreModel; +angular + .module('wcagReporter') + .controller('EvalExploreCtrl', function ( + $scope, + appState, + $timeout, + evalExploreModel + ) { + $scope.state = appState.moveToState('explore'); + $scope.exploreModel = evalExploreModel; - $scope.updateSpec = function (tech) { - if (techMap[tech.title]) { - tech.id = techMap[tech.title]; - } - }; + $scope.updateSpec = function (tech) { + if (techMap[tech.title]) { + tech.id = techMap[tech.title]; + } + }; - $scope.knownTech = angular.copy(evalExploreModel.knownTech); - $scope.otherTech = []; + $scope.knownTech = angular.copy(evalExploreModel.knownTech); + $scope.otherTech = []; - // set relied upon technologies in the right field - evalExploreModel.reliedUponTechnology.forEach(function (tech) { + // set relied upon technologies in the right field + evalExploreModel.reliedUponTechnology + .forEach(function (tech) { var index = $scope.knownTech + // Find exact matching index in knownTech of reliedUponTechnology + // it will be an user defined technology otherwise .reduce(function (index, currTech, currIndex) { if (currTech.id === tech.id && currTech.title === tech.title) { return currIndex; @@ -30,78 +37,81 @@ angular.module('wcagReporter') if (index !== -1) { $scope.knownTech[index].checked = true; } else { - // Push the tech to the other tech field + // Push the tech to the other tech field (it is user defined) $scope.otherTech.push(tech); } }); - // Add an empty field by default - if ($scope.otherTech.length === 0) { - $scope.otherTech.push({ type: 'Technology' }); + // Add an empty field by default + if ($scope.otherTech.length === 0) { + $scope.otherTech.push({ type: 'Technology' }); + } else { + $scope.rootHide.OtherTech = $scope.rootHide.OtherTech || true; + } + + $scope.changeTech = function (tech) { + if (tech.checked) { + var newTech = angular.extend({}, tech); + delete newTech.checked; + evalExploreModel.reliedUponTechnology.push(newTech); } else { - $scope.rootHide.OtherTech = $scope.rootHide.OtherTech || true; + evalExploreModel.reliedUponTechnology = evalExploreModel.reliedUponTechnology + .filter(function (item) { + return item.title !== tech.title && item.id !== tech.id; + }); } + }; - $scope.changeTech = function (tech) { - if (tech.checked) { - var newTech = angular.extend({}, tech); - delete newTech.checked; - evalExploreModel.reliedUponTechnology.push(newTech); - } else { - evalExploreModel.reliedUponTechnology = evalExploreModel.reliedUponTechnology - .filter(function (item) { - return item.title !== tech.title && item.id !== tech.id; - }); - } - }; + $scope.updateOtherTech = function (tech) { + var index = evalExploreModel.reliedUponTechnology.indexOf(tech); + var isEmpty = !tech.title && !tech.id; + if (index === -1 && !isEmpty) { + evalExploreModel.reliedUponTechnology.push(tech); + } else if (index !== -1 && isEmpty) { + evalExploreModel.reliedUponTechnology.splice(index, 1); + } + }; - $scope.updateOtherTech = function (tech) { - var index = evalExploreModel.reliedUponTechnology.indexOf(tech); - var isEmpty = !tech.title && !tech.id; - if (index === -1 && !isEmpty) { - evalExploreModel.reliedUponTechnology.push(tech); - } else if (index !== -1 && isEmpty) { - evalExploreModel.reliedUponTechnology.splice(index, 1); - } - }; + $scope.addTechnology = function ($event) { + $scope.otherTech.push({ type: 'Technology' }); - $scope.addTechnology = function ($event) { - $scope.otherTech.push({ type: 'Technology' }); + // evalExploreModel.addReliedUponTech(); + if ($event) { + var button = angular.element($event.delegateTarget); - // evalExploreModel.addReliedUponTech(); - if ($event) { - var button = angular.element($event.delegateTarget); - $timeout(function () { - var inputs = button.prev() - .find('input'); - inputs[inputs.length - 2].select(); - }, 100); - } - }; + $timeout(function () { + var inputs = button.prev() + .find('input'); - $scope.removeTechnology = function ($index, $event) { - var tech = $scope.otherTech[$index]; - var index = evalExploreModel.reliedUponTechnology.indexOf(tech); - evalExploreModel.reliedUponTechnology.splice(index, 1); - $scope.otherTech.splice($index, 1); + if (inputs.length > 0) { + inputs[0].select(); + } + }); + } + }; - // evalExploreModel.reliedUponTechnology.splice($index,1); - // We need this timeout to prevent Angular UI from throwing an error - if ($event) { - $timeout(function () { - angular.element($event.delegateTarget) - .closest('fieldset') - .parent() - .children() - .last() - .focus(); - }); - } - }; + $scope.removeTechnology = function ($index, $event) { + var tech = $scope.otherTech[$index]; + var index = evalExploreModel.reliedUponTechnology.indexOf(tech); + evalExploreModel.reliedUponTechnology.splice(index, 1); + $scope.otherTech.splice($index, 1); - var techMap = {}; - $scope.knownTech.forEach(function (knownTech) { - techMap[knownTech.title] = knownTech.id; - }); - } - ); + // evalExploreModel.reliedUponTechnology.splice($index,1); + // We need this timeout to prevent Angular UI from throwing an error + if ($event) { + $timeout(function () { + angular.element($event.delegateTarget) + .closest('fieldset') + .parent() + .children() + .last() + .focus(); + }); + } + }; + + var techMap = {}; + $scope.knownTech.forEach(function (knownTech) { + techMap[knownTech.title] = knownTech.id; + }); + }); diff --git a/app/scripts/controllers/evaluation/scope.js b/app/scripts/controllers/evaluation/scope.js index 8e508e7e..140b17e6 100644 --- a/app/scripts/controllers/evaluation/scope.js +++ b/app/scripts/controllers/evaluation/scope.js @@ -13,6 +13,15 @@ angular.module('wcagReporter') $scope.state = appState.moveToState('scope'); $scope.scopeModel = evalScopeModel; + $scope.wcagVersionOptions = evalScopeModel.wcagVersionOptions + .reduce(function (versions, version) { + var translateKey = 'SCOPE.' + version; + + versions[version] = $filter('translate')(translateKey); + + return versions; + }, {}); + $scope.conformanceOptions = evalScopeModel.conformanceOptions .reduce(function (tgt, lvl) { tgt[lvl] = $filter('rdfToLabel')(lvl); diff --git a/app/scripts/controllers/import.js b/app/scripts/controllers/import.js new file mode 100644 index 00000000..42bb2788 --- /dev/null +++ b/app/scripts/controllers/import.js @@ -0,0 +1,392 @@ +'use strict'; + +angular + .module('wcagReporter') + .controller('ImportCtrl', function ( + fileReader, + $scope, + $rootScope, + evalContextV3, + evalModel, + types, + isObjectLiteral, + wcagSpecIdMap + ) { + var JSONLD = window.jsonld; + var FEEDBACK = { + ERROR: { + type: 'error', + class: 'danger' + }, + PENDING: { + type: 'pending', + class: 'info' + }, + SUCCESS: { + type: 'success', + class: 'success' + } + }; + + $scope.assertionImport = []; + + $scope.allowedMime = [ + 'application/json', + 'application/ld+json' + ].join(','); + + $scope.feedback = false; + $scope.importFile = undefined; + $scope.importConfirmed = undefined; + + /** + * Assertions that get imported need to be validated against + * 1. test: should be directly known / related to WCAG + * 2. subject should be related to one of the samples + * 3. result: being an earl:TestResult + * 4. assertedBy: Nice to know who / what made this assertion + * @param {earl:Assertion} assertion [description] + * @return {boolean} validity + */ + function isValidAssertion (assertion) { + function hasRequiredKeys (_assertion) { + var assertionKeys = Object.keys(_assertion); + var requiredKeys = [ + 'test', + 'subject', + 'result', + 'assertedBy' + ]; + + var key; + + for (key in requiredKeys) { + if (assertionKeys.indexOf(requiredKeys[key]) === -1) { + return false; + } + } + + return true; + } + + function isSampleRelated (subject) { + var sampleUrls = evalModel.sampleModel.getPages() + .map(function getUrls (page) { + var pageUrl; + + if (page.source !== undefined) { + try { + pageUrl = new URL(page.source); + } catch (e) { + console.error(e); + + return page.source; + } + + return pageUrl.href; + } + }); + + var subjectUrl = ''; + + if (typeof subject === 'string') { + try { + subjectUrl = new URL(subject).href; + } catch (e) { + console.error('Expected valid url in import assertion subject.'); + + return false; + } + } + + if ( + isObjectLiteral(subject) && + subject.source !== undefined + ) { + try { + subjectUrl = new URL(subject.source).href; + } catch (e) { + console.error('Expected valid url in import assertion subject.'); + + return false; + } + } + + return (sampleUrls.indexOf(subjectUrl) >= 0); + } + + function isWcagRelated (assertionTest) { + if ( + typeof assertionTest === 'string' && + isWcagId(assertionTest) + ) { + return true; + } + + if ( + isObjectLiteral(assertionTest) && + assertionTest.id !== undefined && + isWcagId(assertionTest.id) + ) { + setWcagId(assertion, assertionTest.id); + return true; + } + + if ( + isObjectLiteral(assertionTest) && + assertionTest.isPartOf !== undefined && + typeof assertionTest.isPartOf === 'string' && + isWcagId(assertionTest.isPartOf) + ) { + setWcagId(assertion, assertionTest.isPartOf); + return true; + } + + return false; + } + + function hasResult (_assertion) { + var result = _assertion.result; + + function hasOutcomeValue (_result) { + var earlOutcome = types.EARL.OUTCOME; + var outcomeValues = [ + earlOutcome.PASSED, + earlOutcome.FAILED, + earlOutcome.CANT_TELL, + earlOutcome.INAPPLICABLE, + earlOutcome.UNTESTED + ]; + var outcomeClasses = [ + earlOutcome.PASS, + earlOutcome.FAIL, + earlOutcome.CANNOT_TELL, + earlOutcome.NOT_APPLICABLE, + earlOutcome.NOT_TESTED + ]; + + if (_result.outcome === undefined) { + return false; + } + + if ( + typeof _result.outcome === 'string' && + outcomeValues.indexOf(_result.outcome) >= 0 + ) { + return true; + } + + if ( + isObjectLiteral(_result.outcome) && + _result.outcome['@type'] !== undefined && + outcomeClasses.indexOf(_result.outcome['@type']) >= 0 + ) { + return true; + } + } + + if (!hasOutcomeValue(result)) { + return false; + } + + return true; + } + + if (!hasRequiredKeys(assertion)) { + return false; + } + + if (!isSampleRelated(assertion.subject)) { + return false; + } + + if (!isWcagRelated(assertion.test)) { + return false; + } + + if (!hasResult(assertion)) { + return false; + } + + return true; + } + + function isWcagId (testId) { + var _id = testId.split(':')[1]; + + // Find existing wcag id + return wcagSpecIdMap.some(function (wcagIdSet) { + return wcagIdSet.indexOf(_id) >= 0; + }); + } + + function upgradeWcagId (wcagId) { + var _id = wcagId.split(':')[1]; + var wcagIdSet = wcagSpecIdMap.filter(function (idSet) { + return idSet.indexOf(_id) >= 0; + })[0]; + var idCount = wcagIdSet.length; + + return 'WCAG2:' + wcagIdSet[idCount - 1]; + } + + function setWcagId (assertion, wcagId) { + var wcagVersion = wcagId.split(':')[0]; + + if (wcagVersion !== 'WCAG2') { + wcagId = upgradeWcagId(wcagId); + } + assertion.wcagId = wcagId; + } + + function getWcagId (assertion) { + if (typeof assertion.test === 'string') { + return assertion.test; + } + + return assertion.wcagId || false; + } + + /** + * Tries to insert all found assertions from the import + * into the auditModel specific criteria + */ + function insertAssertions () { + var assertions = $scope.assertionImport; + var assertionsCount = assertions.length; + var assertion, wcagId; + + for (var i = 0; i < assertionsCount; i++) { + assertion = assertions[i]; + wcagId = getWcagId(assertion); + + if (wcagId) { + evalModel.auditModel.updateCritAssert(wcagId, assertion); + } + } + + $scope.feedback = FEEDBACK.SUCCESS; + $scope.feedback.message = 'Import successfull! Imported ' + assertionsCount + ' assertions.'; + } + + function resetImport () { + $scope.feedback = false; + $scope.importFile = undefined; + $scope.importConfirmed = undefined; + $scope.assertionImport.length = 0; + } + + function handleLoad (defer, feedback) { + defer.then( + function success (result) { + var resultJson = JSON.parse(result); + var context = angular.copy(evalContextV3); + context.WCAG20 = 'https://www.w3.org/TR/WCAG20/#'; + context.isPartOf = { + '@id': 'dct:isPartOf', + '@type': '@id' + }; + + JSONLD.frame( + resultJson, + { + '@context': context, + '@graph': [ + { + '@type': 'Assertion' + } + ] + }, + function (error, framed) { + if (error) { + feedback = FEEDBACK.ERROR; + feedback.message = error.message; + return; + } + + var graph = framed['@graph']; + var graphSize = graph.length; + var currentAssertion; + + for (var i = 0; i < graphSize; i++) { + currentAssertion = graph[i]; + + if (isValidAssertion(currentAssertion)) { + $scope.assertionImport.push(currentAssertion); + } + } + + if ($scope.assertionImport.length > 0) { + $scope.feedback = FEEDBACK.PENDING; + $scope.feedback.message = 'Ready to import ' + $scope.assertionImport.length + ' assertions.'; + } else { + $scope.feedback = FEEDBACK.ERROR; + $scope.feedback.message = 'No Assertions found in file “' + $scope.importFile.name + '”'; + + $scope.importFile = null; + } + + $scope.$apply(); + } + ); + }, + function error (e) { + feedback = FEEDBACK.ERROR; + if (e.message) { + feedback.message = e.message; + } else { + feedback.message = e; + } + } + ); + } + + function isJson (file) { + if ($scope.allowedMime.indexOf(file.type) >= 0) { + return true; + } + + return false; + } + + $scope.loadFile = function loadFile (source) { + $scope.feedback = FEEDBACK.PENDING; + + if (!isJson(source)) { + $scope.feedback = FEEDBACK.ERROR; + $scope.feedback.message = 'Expected to open a json-file, the filename must end with either “.json” or “.jsonld”.'; + $scope.$apply(); + + return; + } + + $scope.importFile = { + name: source.name + }; + + handleLoad(fileReader.readAsText(source, $scope), $scope.feedback); + }; + + $scope.handleConfirmation = function handleConfirmation (confirmed) { + if (confirmed === undefined) { + confirmed = false; + } + + if (confirmed) { + $scope.feedback = FEEDBACK.PENDING; + $scope.feedback.message = 'Inserting ' + $scope.assertionImport.length + ' assertions from “' + $scope.importFile.name + '”'; + + insertAssertions(); + + $scope.importConfirmed = confirmed; + } else { + resetImport(); + $scope.feedback = FEEDBACK.PENDING; + $scope.feedback.message = 'Import aborted. Choose another file or go back to the evaluation.'; + } + }; + + $scope.handleDoneClick = function handleDoneClick () { + $rootScope.setEvalLocation(); + }; + }); diff --git a/app/scripts/models/class/CriterionAssert.js b/app/scripts/models/class/CriterionAssert.js index f1296c2e..bc8e1725 100644 --- a/app/scripts/models/class/CriterionAssert.js +++ b/app/scripts/models/class/CriterionAssert.js @@ -2,6 +2,7 @@ angular.module('wcagReporter') .service('CriterionAssert', function ( + types, evalSampleModel, $filter, TestCaseAssert, @@ -19,9 +20,11 @@ angular.module('wcagReporter') } this.test = idref; + this.mode = types.EARL.MODE.MANUAL; this.hasPart = []; this.result = { - outcome: 'earl:untested', + type: types.EARL.RESULT.class, + outcome: types.EARL.OUTCOME.UNTESTED, description: '' }; @@ -161,12 +164,12 @@ angular.module('wcagReporter') }, false); return hasPart || !!critAssert.result.description || - critAssert.result.outcome !== 'earl:untested'; + critAssert.result.outcome !== types.EARL.OUTCOME.UNTESTED; }; CriterionAssert.updateMetadata = function (critAssert) { critAssert.assertedBy = currentUser.id; - critAssert.mode = 'earl:manual'; + critAssert.mode = types.EARL.MODE.MANUAL; critAssert.result.date = $filter('date')(Date.now(), 'yyyy-MM-dd HH:mm:ss Z'); }; diff --git a/app/scripts/models/class/TestCaseAssert.js b/app/scripts/models/class/TestCaseAssert.js index 561d28d8..80e3bf11 100644 --- a/app/scripts/models/class/TestCaseAssert.js +++ b/app/scripts/models/class/TestCaseAssert.js @@ -1,10 +1,16 @@ 'use strict'; -angular.module('wcagReporter') - .service('TestCaseAssert', function (evalSampleModel, currentUser) { +angular + .module('wcagReporter') + .service('TestCaseAssert', function ( + types, + evalSampleModel, + currentUser + ) { var protoResult = { + type: types.EARL.RESULT.class, description: '', - outcome: 'earl:untested' + outcome: types.EARL.OUTCOME.UNTESTED }; function TestCaseAssert () { @@ -16,7 +22,7 @@ angular.module('wcagReporter') } this.subject = []; - this.result = Object.create(protoResult); + this.result = angular.copy(protoResult); } TestCaseAssert.isDefined = function (tc) { @@ -34,7 +40,7 @@ angular.module('wcagReporter') testCase: undefined, result: undefined, multiPage: false, - mode: 'earl:manual', + mode: types.EARL.MODE.MANUAL, isDefined: function () { return TestCaseAssert.isDefined(this); }, diff --git a/app/scripts/models/evaluation.js b/app/scripts/models/evaluation.js index 2aa49d68..bd31cf5b 100644 --- a/app/scripts/models/evaluation.js +++ b/app/scripts/models/evaluation.js @@ -1,22 +1,20 @@ 'use strict'; -/** - * - */ -angular.module('wcagReporter') +angular + .module('wcagReporter') .factory('evalModel', function ( evalScopeModel, evalExploreModel, evalSampleModel, evalAuditModel, evalReportModel, - evalContextV2, + evalContextV3, currentUser ) { var evalModel = { id: undefined, type: 'Evaluation', - context: evalContextV2, + context: evalContextV3, scopeModel: evalScopeModel, exploreModel: evalExploreModel, sampleModel: evalSampleModel, diff --git a/app/scripts/models/evaluation/audit.js b/app/scripts/models/evaluation/audit.js index 823949d4..5331925d 100644 --- a/app/scripts/models/evaluation/audit.js +++ b/app/scripts/models/evaluation/audit.js @@ -5,7 +5,9 @@ angular.module('wcagReporter') TestCaseAssert, evalScopeModel, wcag2spec, - CriterionAssert + CriterionAssert, + types, + $filter ) { var auditModel; var criteria = {}; @@ -21,6 +23,38 @@ angular.module('wcagReporter') }); }); + function updateAssertion (assertion, update) { + var testResult = update.result; + + function composeImportResult (result) { + var composed = '\n\n'; + composed += '*Imported finding*'; + composed += '\noutcome: ' + $filter('rdfToLabel')(result.outcome); + if (result.description) { + composed += '\n' + result.description; + } + + return composed; + } + + assertion.result.description += composeImportResult(testResult); + + // Remove empty lines at start of description + assertion.result.description = assertion.result.description.replace(/^\s+/, ''); + + // Decide what outcome should be set. + // Set Failed if imported result is Failed + // This forces the evaluator to check the import and this is the only outcome + // that can be set with certainty by automatic assertors. + if ( + // Dont try to modify if it already has failed outcome + assertion.result.outcome !== types.EARL.OUTCOME.FAILED && + testResult.outcome === types.EARL.OUTCOME.FAILED + ) { + assertion.result.outcome = types.EARL.OUTCOME.FAILED; + } + } + auditModel = { criteria: criteria, @@ -61,8 +95,9 @@ angular.module('wcagReporter') if (!angular.isArray(evalData.auditResult)) { evalData.auditResult = [evalData.auditResult]; } - criteria = {}; - auditModel.criteria = criteria; + // NOTE: Why was this done? (Reset criteria to imported criteria) + // criteria = {}; + // auditModel.criteria = criteria; evalData.auditResult.forEach(auditModel.addCritAssert); } @@ -105,6 +140,21 @@ angular.module('wcagReporter') criteria[newCrit.test] = newCrit; }, + updateCritAssert: function updateCritAssert (id, data) { + if (data === undefined) { + return; + } else if (typeof data !== 'object') { + return; + } + + // First try to get a matching criteria before anything else + var criterion = auditModel.getCritAssert(id); + + if (data.result) { + updateAssertion(criterion, data); + } + }, + addPageForAsserts: function (page) { Object.keys(criteria) .forEach(function (critName) { diff --git a/app/scripts/models/evaluation/explore.js b/app/scripts/models/evaluation/explore.js index 6bc405f0..5fe9be05 100644 --- a/app/scripts/models/evaluation/explore.js +++ b/app/scripts/models/evaluation/explore.js @@ -1,22 +1,27 @@ 'use strict'; -angular.module('wcagReporter') - .service('evalExploreModel', function (knownTech, evalSampleModel) { +angular + .module('wcagReporter') + .service('evalExploreModel', function ( + knownTech, + evalSampleModel + ) { var exploreModel = { - commonPages: [], - otherRelevantPages: [], knownTech: knownTech }; var basicProps = [ 'reliedUponTechnology', 'essentialFunctionality', - 'pageTypeVariety' + 'pageTypeVariety', + 'commonPages', + 'otherRelevantPages' ]; // add all properties to this - basicProps.forEach(function (prop) { - exploreModel[prop] = undefined; - }); + basicProps + .forEach(function (prop) { + exploreModel[prop] = undefined; + }); exploreModel.reliedUponTechnology = []; @@ -24,19 +29,23 @@ angular.module('wcagReporter') if (!angular.isArray(evalData.reliedUponTechnology)) { evalData.reliedUponTechnology = [evalData.reliedUponTechnology]; } - basicProps.forEach(function (prop) { - if (evalData[prop]) { - exploreModel[prop] = evalData[prop]; - } - }); + + basicProps + .forEach(function (prop) { + if (evalData[prop]) { + exploreModel[prop] = evalData[prop]; + } + }); }; exploreModel.exportData = function () { var exportData = {}; - basicProps.forEach(function (prop) { - exportData[prop] = exploreModel[prop]; - }); + basicProps + .forEach(function (prop) { + exportData[prop] = exploreModel[prop]; + }); + return exportData; }; diff --git a/app/scripts/models/evaluation/scope.js b/app/scripts/models/evaluation/scope.js index bd20aa5e..1b8ac5a4 100644 --- a/app/scripts/models/evaluation/scope.js +++ b/app/scripts/models/evaluation/scope.js @@ -4,7 +4,8 @@ angular.module('wcagReporter') .service('evalScopeModel', function () { var scopeModel = { type: 'EvaluationScope', - conformanceTarget: 'wai:WCAG2AA-Conformance', + wcagVersion: 'WCAG21', + conformanceTarget: 'wai:WCAG2AA-Conformance', additionalEvalRequirement: '', website: { type: [ @@ -33,6 +34,11 @@ angular.module('wcagReporter') }; }; + scopeModel.wcagVersionOptions = [ + 'WCAG21', + 'WCAG20' + ]; + scopeModel.conformanceOptions = [ 'wai:WCAG2A-Conformance', 'wai:WCAG2AA-Conformance', diff --git a/app/scripts/models/import.js b/app/scripts/models/import.js index d0e47d58..034d7053 100644 --- a/app/scripts/models/import.js +++ b/app/scripts/models/import.js @@ -1,178 +1,222 @@ 'use strict'; -/** - * - */ -angular.module('wcagReporter') - .factory( - 'wcagReporterImport', - function ($rootScope, evalModel, currentUser, reportStorage, importV1, changeLanguage) { - var jsonld = window.jsonld; - - function objectCollide (obj1, obj2) { - Object.keys(obj1) - .forEach(function (prop) { - if (typeof obj1[prop] !== 'function' && - typeof obj2[prop] !== 'undefined') { - obj1[prop] = obj2[prop]; - } - }); - } - - function compactEach (callback) { - var testCallback; - var results = []; - var calls = 0; - var evalType = evalModel.context['@vocab'] + evalModel.type; - var personType = currentUser['@context']['@vocab'] + currentUser.type; - - testCallback = function (err, compacted) { - results.push(compacted); - if (results.length === calls) { - callback(results); - } - }; - - return function (evalObj) { - calls += 1; - - if (evalObj['@type'] && - evalObj['@type'].indexOf(evalType) !== -1) { - // Compact with the evaluation context - jsonld.compact( - evalObj, - evalModel.context, - testCallback - ); - } else if (evalObj['@type'] && - evalObj['@type'].indexOf(personType) !== -1) { - // Compact with the FOAF context - jsonld.compact( - evalObj, - currentUser['@context'], - testCallback - ); - } else { - results.push(evalObj); +angular + .module('wcagReporter') + .factory('wcagReporterImport', function ( + $rootScope, + evalModel, + currentUser, + reportStorage, + importV1, + changeLanguage + ) { + var jsonld = window.jsonld; + + /** + * OBJECT MODIFIER + * Add to or replace object 1's keys with object 2's keys + * @param {Object} obj1 Object that needs to be updated + * @param {Object} obj2 Object with keys that need to replace or be added to obj1 + * @return {undefined} Modifies object 1 with object 2 keys + */ + function objectCollide (obj1, obj2) { + Object.keys(obj1) + .forEach(function (prop) { + if ( + typeof obj1[prop] !== 'function' && + typeof obj2[prop] !== 'undefined' + ) { + obj1[prop] = obj2[prop]; } - }; + }); + } + + function compactEach (callback) { + var results = []; + var calls = 0; + var evalType = evalModel.context['@vocab'] + evalModel.type; + var personType = currentUser['@context']['@vocab'] + currentUser.type; + + function testCallback (err, compacted) { + if (err) { + // Something json-ldish is not ok here, exit. + // This should not be the case anytime since the data should have + // been checked before importing. + console.error(err); + return; + } + + results.push(compacted); + + if (results.length === calls) { + callback(results); + } } - /** + return function (evalObj) { + calls += 1; + + if ( + evalObj['@type'] && + evalObj['@type'].indexOf(evalType) !== -1 + ) { + // Compact with the evaluation context + jsonld.compact( + evalObj, + evalModel.context, + testCallback + ); + } else if ( + evalObj['@type'] && + evalObj['@type'].indexOf(personType) !== -1 + ) { + // Compact with the FOAF context + jsonld.compact( + evalObj, + currentUser['@context'], + testCallback + ); + } else { + results.push(evalObj); + } + }; + } + + /** * Inject evaluation data into the reporter * @param {[Object]} evalData */ - function updateEvalModel (evalData) { - if (evalData.evaluationScope) { - objectCollide(evalModel.scopeModel, evalData.evaluationScope); - } + function updateEvalModel (evalData) { + if (evalData.evaluationScope) { + objectCollide(evalModel.scopeModel, evalData.evaluationScope); + } - evalModel.id = evalData.id; - evalModel.type = evalData.type; + evalModel.id = evalData.id; + evalModel.type = evalData.type; - evalModel.sampleModel.importData(evalData); + evalModel.sampleModel.importData(evalData); + evalModel.reportModel.importData(evalData); + evalModel.auditModel.importData(evalData); + evalModel.exploreModel.importData(evalData); + evalModel.otherData = evalData.otherData; + } - evalModel.reportModel.importData(evalData); + var importModel = { - evalModel.auditModel.importData(evalData); - evalModel.exploreModel.importData(evalData); - evalModel.otherData = evalData.otherData; - } + storage: reportStorage, - var importModel = { - - storage: reportStorage, - - /** - * Import an evaluation from a JSON string - * @param {string} json Evaluation - */ - fromJson: function (json) { - importModel.fromObject(angular.fromJson(json)); - }, - - getFromUrl: function () { - return reportStorage.get() - .then(function (data) { - importModel.fromJson(data); - return data; - }); - }, - - fromObject: function (evalData) { - // Check if an old format needs to be converted: - if (angular.isArray(evalData['@graph']) && - typeof evalData['@graph'][0] === 'object' && - evalData['@graph'][0].type.toLowerCase() === 'evaluation') { - // Fix an older import format - evalData['@graph'] = importV1(evalData['@graph']); - } - jsonld.expand(evalData, function (err, expanded) { - if (err) { - console.error(err); - } - importModel.fromExpanded(expanded); + /** + * Import an evaluation from a JSON string + * @param {string} json Evaluation + * @return {undefined} + */ + fromJson: function (json) { + importModel.fromObject(angular.fromJson(json)); + }, + + getFromUrl: function () { + return reportStorage.get() + .then(function (data) { + importModel.fromJson(data); + return data; }); - }, - - fromExpanded: function (evalData) { - evalData.forEach(compactEach(function (results) { - var evaluation = results.reduce(function (result, data) { - if (data.type === 'Evaluation') { - if (typeof result !== 'undefined') { - throw new Error('Only one evaluation object allowed in JSON data'); - } - return data; - } - return result; - }, undefined); + }, + + fromObject: function (evalData) { + // Check if an old format needs to be converted: + var graphData = evalData['@graph'] || null; + + if ( + angular.isArray(graphData) && + !importV1.isLatestVersion(graphData) + ) { + // Fix an older import format + evalData['@graph'] = importV1(graphData); + } - if (!evaluation) { - throw new Error('No evaluation found in data'); - } + jsonld.expand(evalData, function (err, expanded) { + if (err) { + console.error(err); + } - // If the creator has an id, give that id to the current user - if (typeof evaluation.creator === 'string' && - evaluation.creator.indexOf('_:') === 0) { - currentUser.id = evaluation.creator; - } - evaluation.creator = currentUser; - var foundUser = false; - // Find the first Person that matches the ID of the current user - results.forEach(function (data) { - if (!foundUser && data.type === 'Person' && - data.id === currentUser.id) { - // overwrite the current user with the new data - angular.extend(currentUser, data); - foundUser = true; + importModel.fromExpanded(expanded); + }); + }, + + fromExpanded: function (evalData) { + evalData + .forEach( + compactEach(function (results) { + var evaluation = results + .reduce(function (result, data) { + if (data.type === 'Evaluation') { + if (typeof result !== 'undefined') { + throw new Error('Only one evaluation object allowed in JSON data'); + } + + return data; + } + + return result; + }, undefined); + + if (!evaluation) { + throw new Error('No evaluation found in data'); } - }); - // Take all data that isn't the evaluation or the current user - evaluation.otherData = results.reduce(function (otherData, data) { - if (data !== evaluation && data.id !== currentUser.id) { - otherData.push(data); + // If the creator has an id, give that id to the current user + if (typeof evaluation.creator === 'string' && + evaluation.creator.indexOf('_:') === 0) { + currentUser.id = evaluation.creator; } - return otherData; - }, [currentUser]); - - if (evaluation.lang) { - // This is a workaround for what seems to be a bug in the - // JSON-LD lib. It outputs ['e', 'n'] instead of 'en', so we - // join to fix this. - if (angular.isArray(evaluation.lang)) { - evaluation.lang = evaluation.lang.join(''); + + evaluation.creator = currentUser; + + var foundUser = false; + + // Find the first Person that matches the ID of the current user + results + .forEach(function (data) { + if ( + !foundUser && + data.type === 'Person' && + data.id === currentUser.id + ) { + // overwrite the current user with the new data + angular.extend(currentUser, data); + foundUser = true; + } + }); + + // Take all data that isn't the evaluation or the current user + evaluation.otherData = results + .reduce(function (otherData, data) { + if (data !== evaluation && data.id !== currentUser.id) { + otherData.push(data); + } + + return otherData; + }, [currentUser]); + + if (evaluation.lang) { + // This is a workaround for what seems to be a bug in the + // JSON-LD lib. It outputs ['e', 'n'] instead of 'en', so we + // join to fix this. + if (angular.isArray(evaluation.lang)) { + evaluation.lang = evaluation.lang.join(''); + } + + changeLanguage(evaluation.lang); } - changeLanguage(evaluation.lang); - } - // Put the evaluation as the first on the list - $rootScope.$apply(function () { - updateEvalModel(evaluation); - }); - })); - } - }; - return importModel; - } - ); + // Put the evaluation as the first on the list + $rootScope.$apply(function () { + updateEvalModel(evaluation); + }); + }) + ); + } + }; + + return importModel; + }); diff --git a/app/scripts/models/import/importV1.js b/app/scripts/models/import/importV1.js index c1af0a9c..25214c1a 100644 --- a/app/scripts/models/import/importV1.js +++ b/app/scripts/models/import/importV1.js @@ -1,23 +1,68 @@ 'use strict'; -angular.module('wcagReporter') - .factory('importV1', function (evalContextV1, evalContextV2, $filter) { - var getUrl = $filter('getUrl'); +/** + * ImportV1; imports and migrates jsonld data + * TODO: Use JSONLD API + */ +angular + .module('wcagReporter') + .factory('importV1', function ( + types, + wcagSpecIdMap, + evalContextV1, + evalContextV2, + evalContextV3, + $filter + ) { + var getUrl = $filter('getUrl'); + var isLatestVersion = isV3Evaluation; + /** + * Converts json-ld @graph contents + * @param {Array} '@graph'-contents + * @return {Array} new updated '@graph'-contents + */ function convertor (importArray) { - return importArray.map(function (importObj) { - // upgrade from v1 to v2 - if (isV1Evaluation(importObj)) { - importObj = upgradeToV2(importObj); + return importArray + .map(function (importObj) { + // upgrade from v1 to v2 + if (isV1Evaluation(importObj)) { + importObj = upgradeToV2(importObj); + } + + if (isV2Evaluation(importObj)) { + importObj = upgradeToV3(importObj); + } // Correct the foaf namespace - } else if (typeof importObj === 'object' && - typeof importObj['@context'] === 'object' && - importObj['@context']['@vocab'] === 'http://xmlns.com/foaf/spec/#') { - importObj['@context']['@vocab'] = 'http://xmlns.com/foaf/0.1/'; - } - return importObj; - }); + if ( + typeof importObj === 'object' && + typeof importObj['@context'] === 'object' && + importObj['@context']['@vocab'] === 'http://xmlns.com/foaf/spec/#' + ) { + importObj['@context']['@vocab'] = 'http://xmlns.com/foaf/0.1/'; + } + + return importObj; + }); + } + + /** + * Updates the test WCAG ID to latest version. + * The test ID exist of 2 parts: [WCAG2, ] + * @param {String} test string + * @return {String} new test ID string + */ + function updateTestId (test) { + var testId = test.split(':'); + var criterionIdSet = wcagSpecIdMap + .filter(function (idSet) { + return idSet.indexOf(testId[1]) >= 0; + })[0]; + var latestId = criterionIdSet.length - 1; + testId[1] = criterionIdSet[latestId].toString(); + + return testId.join(':'); } /** @@ -25,55 +70,18 @@ angular.module('wcagReporter') */ function isV1Evaluation (data) { if (typeof data !== 'object') { - throw new TypeError('Expected object for ' + data); + throw new TypeError('Expected data to be of type object but is ' + typeof data + ' instead.'); } + var dataContext = data['@context']; - var contextProps = Object.keys(evalContextV1); + // Skip if the context isn't there if (typeof dataContext !== 'object') { return false; } - // Dirty check if they have the same keys - if (contextProps.sort() - .join(',') !== Object.keys(dataContext) - .sort() - .join(',')) { - return false; - } - - return contextProps.reduce(function (result, prop) { - if (!result) { // false is false - return result; - - // Context prop doesn't exist - } else if (typeof dataContext[prop] === 'undefined') { - return false; - - // Context prop is different value - } else if (typeof dataContext[prop] === 'string' && - dataContext[prop] !== evalContextV1[prop]) { - return false; - - // Context prop is an object, compare it's content - } else if (typeof dataContext[prop] === 'object') { - return Object.keys(evalContextV1[prop]) - .reduce(function (result, subProp) { - if (!result) { - return result; - } else if (typeof dataContext[prop][subProp] === 'undefined') { - return false; - } else if (typeof dataContext[prop][subProp] === 'string' && - dataContext[prop][subProp] !== evalContextV1[prop][subProp]) { - return false; - } else { - return true; - } - }, true); - } else { - return true; - } - }, true); + // Check if full context V1 is represented in dataContext + return _atLeastEqualTo(dataContext, evalContextV1); } /** Upgrade Page to v2 */ @@ -84,88 +92,188 @@ angular.module('wcagReporter') 'WebPage' ]; } + page.title = page.handle; delete page.handle; + var source = getUrl(page.description); if (source) { page.source = source; } } - /** - * Evaluation object from v1 to v2 - */ function upgradeToV2 (evaluation) { - // Replace with the v2 context - evaluation['@context'] = evalContextV2; - - // Capitalize Evaluation - evaluation.type = evaluation.type.replace('evaluation', 'Evaluation'); + // Initiate update to prevent side-effect alteration of evaluation + var update = angular.copy(evaluation); + update['@context'] = evalContextV2; + update.type = 'Evaluation'; // Update the EvaluationScope object - var evalScope = evaluation.evaluationScope; + var evalScope = update.evaluationScope; evalScope.type = evalScope.type || 'EvaluationScope'; evalScope.website.type = evalScope.website.type || [ 'TestSubject', 'WebSite' ]; - evaluation.reliedUponTechnology.forEach(function (tech) { + update.reliedUponTechnology.forEach(function (tech) { tech.type = tech.type || 'Technology'; }); - var evalScope = evaluation.evaluationScope; + // Change conformanceTarget to "wai:WCAG2X-Conformance" where X is A{1,3} if (evalScope.conformanceTarget.substr(0, 13) === 'wcag20:level_') { evalScope.conformanceTarget = 'wai:WCAG2' + ( - evalScope.conformanceTarget.replace('wcag20:level_', '') + evalScope.conformanceTarget + .replace('wcag20:level_', '') .toUpperCase() ) + '-Conformance'; } + + // website.title > website.siteName if (evalScope.website.title) { evalScope.website.siteName = evalScope.website.title; delete evalScope.website.title; } - // Update the sample - if (!angular.isArray(evaluation.structuredSample.webpage)) { - evaluation.structuredSample.webpage = [evaluation.structuredSample.webpage]; + // Update the structured and random sample + if (!angular.isArray(update.structuredSample.webpage)) { + update.structuredSample.webpage = [update.structuredSample.webpage]; } - evaluation.structuredSample.type = evaluation.structuredSample.type || 'Sample'; - evaluation.structuredSample.webpage.forEach(fixPage); + update.structuredSample.type = update.structuredSample.type || 'Sample'; + update.structuredSample.webpage.forEach(fixPage); - if (!angular.isArray(evaluation.randomSample.webpage)) { - evaluation.randomSample.webpage = [evaluation.randomSample.webpage]; + if (!angular.isArray(update.randomSample.webpage)) { + update.randomSample.webpage = [update.randomSample.webpage]; } - evaluation.randomSample.type = evaluation.randomSample.type || 'Sample'; - evaluation.randomSample.webpage.forEach(fixPage); + update.randomSample.type = update.randomSample.type || 'Sample'; + update.randomSample.webpage.forEach(fixPage); // Update assertions - evaluation.auditResult.forEach(function updateAsserts (assertion) { + update.auditResult.forEach(function updateAsserts (assertion) { assertion.type = assertion.type.replace('earl:assertion', 'Assertion'); if (assertion.testRequirement) { assertion.test = assertion.testRequirement.replace('wcag20:', 'WCAG2:'); delete assertion.testRequirement; - } else { + } else if (assertion.testcase) { assertion.test = assertion.testcase.replace('wcag20:', 'WCAG2:'); delete assertion.testcase; } - assertion.result.type = assertion.result.type || 'TestResult'; - if (assertion.mode === 'manual') { - assertion.mode = 'earl:manual'; + if (assertion.result.type !== types.EARL.RESULT.class) { + assertion.result.type = types.EARL.RESULT.class; + } + + if (assertion.mode !== types.EARL.MODE.MANUAL) { + assertion.mode = types.EARL.MODE.MANUAL; } if (assertion.hasPart) { assertion.hasPart.forEach(updateAsserts); } }); - return evaluation; + return update; + } + + function isV2Evaluation (data) { + if (typeof data !== 'object') { + throw new TypeError('Expected object but got ' + typeof data); + } + + if (!Object.prototype.hasOwnProperty.call(data, '@context')) { + return false; + } + + if (data['@context'] === evalContextV2) { + return true; + } + + return _atLeastEqualTo(data['@context'], evalContextV2); + } + + function upgradeToV3 (evaluation) { + var update = angular.copy(evaluation); + + // update context to v3 + update['@context'] = evalContextV3; + + // Update successcriteria ids + update.auditResult + .forEach(function (assertion) { + if (assertion.test) { + assertion.test = updateTestId(assertion.test); + } + + if ( + assertion.hasPart && + assertion.hasPart.length + ) { + assertion.hasPart + .forEach(function (subAssertion) { + if (subAssertion.test) { + subAssertion.test = updateTestId(subAssertion.test); + } + }); + } + }); + + return update; + } + + function isV3Evaluation (data) { + if (typeof data !== 'object') { + throw new TypeError('Expected object but got ' + typeof data); + } + + if (!Object.prototype.hasOwnProperty.call(data, '@context')) { + return false; + } + + if (data['@context'] === evalContextV3) { + return true; + } + + return false; + } + + function _atLeastEqualTo (object1, object2) { + var result = true; + + for (var property in object2) { + if (!Object.prototype.hasOwnProperty.call(object1, property)) { + result = false; + break; + } + + if ( + typeof object1[property] === 'object' && + !_atLeastEqualTo(object1[property], object2[property]) + ) { + result = false; + break; + } + + if ( + typeof object1[property] === 'string' && + object1[property] !== object2[property] + ) { + result = false; + break; + } + } + + return result; } + // Compatibility check + convertor.isLatestVersion = isLatestVersion; + // Expose methods for testing convertor.isV1Evaluation = isV1Evaluation; + convertor.isV2Evaluation = isV2Evaluation; + convertor.isV3Evaluation = isV3Evaluation; convertor.upgradeToV2 = upgradeToV2; + convertor.upgradeToV3 = upgradeToV3; return convertor; }); diff --git a/app/scripts/models/wcag2spec.js b/app/scripts/models/wcag2spec.js index 53978d45..2e88e9d2 100644 --- a/app/scripts/models/wcag2spec.js +++ b/app/scripts/models/wcag2spec.js @@ -65,14 +65,24 @@ angular.module('wcagReporter') // Make an object of the criteria array with uri as keys criteria.forEach(function (criterion) { - if ([ + var levels = [ 'A', 'AA', 'AAA' - ].indexOf(criterion.level) !== -1) { + ]; + + if (levels.indexOf(criterion.level) !== -1) { criterion.level = 'wai:WCAG2' + criterion.level + '-Conformance'; criteriaObj[criterion.id] = criterion; } + + // Versions are 2.0 or 2.1 but need to be json-ld wich is WCAG20 and WCAG21 + if (Object.prototype.hasOwnProperty.call(criterion, 'versions')) { + criterion.versions = criterion.versions + .map(function (version) { + return 'WCAG' + version.replace('.', ''); + }); + } }); broadcast('wcag2spec:langChange', lang); diff --git a/app/scripts/services/context/evalContextV1.js b/app/scripts/services/context/evalContextV1.js index 9f74526c..90f012e7 100644 --- a/app/scripts/services/context/evalContextV1.js +++ b/app/scripts/services/context/evalContextV1.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('wcagReporter') - .value('evalContextV1', { + .constant('evalContextV1', { '@vocab': 'http://www.w3.org/TR/WCAG-EM/#', wcag20: 'http://www.w3.org/TR/WCAG20/#', earl: 'http://www.w3.org/ns/earl#', diff --git a/app/scripts/services/context/evalContextV2.js b/app/scripts/services/context/evalContextV2.js index 999c5575..fc48ba2e 100644 --- a/app/scripts/services/context/evalContextV2.js +++ b/app/scripts/services/context/evalContextV2.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('wcagReporter') - .value('evalContextV2', { + .constant('evalContextV2', { // Current namespace '@vocab': 'http://www.w3.org/TR/WCAG-EM/#', diff --git a/app/scripts/services/context/evalContextV3.js b/app/scripts/services/context/evalContextV3.js new file mode 100644 index 00000000..d6ad57e8 --- /dev/null +++ b/app/scripts/services/context/evalContextV3.js @@ -0,0 +1,107 @@ +'use strict'; + +angular + .module('wcagReporter') + .constant('evalContextV3', { + // Current namespace + '@vocab': 'http://www.w3.org/TR/WCAG-EM/#', + + // Namespaces + reporter: 'https://github.com/w3c/wcag-em-report-tool/', + wcagem: 'http://www.w3.org/TR/WCAG-EM/#', + WCAG2: 'http://www.w3.org/TR/WCAG21/#', + earl: 'http://www.w3.org/ns/earl#', + dct: 'http://purl.org/dc/terms/', + wai: 'http://www.w3.org/WAI/', + sch: 'http://schema.org/', + + // Classes + Evaluation: 'wcagem:Evaluation', + EvaluationScope: 'wcagem:EvaluationScope', + TestSubject: 'earl:TestSubject', + WebSite: 'sch:WebSite', + Sample: 'wcagem:Sample', + WebPage: 'sch:WebPage', + Technology: 'WCAG2:dfn-technologies', + Assertion: 'earl:Assertion', + Assertor: 'earl:Assertor', + TestResult: 'earl:TestResult', + + // Evaluation class properties + title: 'dct:title', + summary: 'dct:summary', + creator: { + '@id': 'dct:creator', + '@type': '@id' + }, + date: 'dct:date', + commissioner: 'wcagem:commissioner', + reliedUponTechnology: 'WCAG2:dfn-relied-upon', + evaluationScope: 'step1', + commonPages: 'step2a', + essentialFunctionality: 'step2b', + pageTypeVariety: 'step2c', + otherRelevantPages: 'step2e', + structuredSample: 'step3a', + randomSample: 'step3b', + auditResult: 'step4', + specifics: 'step5b', + publisher: { + '@id': 'dct:publisher', + '@type': '@id' + }, + + // EvaluationScope class properties + conformanceTarget: { + '@id': 'step1b', + '@type': '@id' + }, + accessibilitySupportBaseline: 'step1c', + additionalEvalRequirement: 'step1d', + website: 'WCAG2:dfn-set-of-web-pages', + + // sch:WebSite class properties + siteScope: 'step1a', + siteName: 'sch:name', + + // Sample class properties + // sch:WebPage class properties + webpage: 'WCAG2:dfn-web-page-s', + description: 'dct:description', + source: { + '@id': 'dct:source', + '@type': '@id' + }, + tested: 'reporter:blob/master/docs/EARL%2BJSON-LD.md#tested', + + // earl:Assertion class properties + test: { + '@id': 'earl:test', + '@type': '@id' + }, + assertedBy: { + '@id': 'earl:assertedBy', + '@type': '@id' + }, + subject: { + '@id': 'earl:subject', + '@type': '@id' + }, + result: 'earl:result', + mode: { + '@id': 'earl:mode', + '@type': '@id' + }, + hasPart: 'dct:hasPart', + + // earl:TestResult class properties + outcome: { + '@id': 'earl:outcome', + '@type': '@id' + }, + + // shorthand, because @ can't be used in dot notation + id: '@id', + type: '@type', + lang: '@language' + }); diff --git a/app/scripts/services/helpers/isObjectLiteral.js b/app/scripts/services/helpers/isObjectLiteral.js new file mode 100644 index 00000000..203c80ab --- /dev/null +++ b/app/scripts/services/helpers/isObjectLiteral.js @@ -0,0 +1,26 @@ +angular + .module('wcagReporter') + .service('isObjectLiteral', function () { + /** + * isObjectLiteral tests if parameter is an object literal like: + * { + * key1: value1, + * …, + * keyN: valueN + * } + * @param {any} test [parameter to test] + * @return {Boolean} + */ + function isObjectLiteral (test) { + if ( + typeof test === 'object' && + Object.prototype.toString.call(test) === '[object Object]' + ) { + return true; + } + + return false; + } + + return isObjectLiteral; + }); diff --git a/app/scripts/services/types.js b/app/scripts/services/types.js new file mode 100644 index 00000000..acec0b0d --- /dev/null +++ b/app/scripts/services/types.js @@ -0,0 +1,30 @@ +'use strict'; + +angular + .module('wcagReporter') + .constant('types', { + EARL: { + type: 'earl', + MODE: { + type: 'earl:TestMode', + MANUAL: 'earl:manual' + }, + OUTCOME: { + type: 'earl:OutcomeValue', + CANNOT_TELL: 'earl:CannotTell', + CANT_TELL: 'earl:cantTell', + INAPPLICABLE: 'earl:inapplicable', + FAIL: 'earl:Fail', + FAILED: 'earl:failed', + NOT_APPLICABLE: 'earl:NotApplicable', + NOT_TESTED: 'earl:NotTested', + PASS: 'earl:Pass', + PASSED: 'earl:passed', + UNTESTED: 'earl:untested' + }, + RESULT: { + class: 'TestResult', + type: 'earl:TestResult' + } + } + }); diff --git a/app/scripts/services/wcagSpecIdMap.js b/app/scripts/services/wcagSpecIdMap.js new file mode 100644 index 00000000..ee6b04a1 --- /dev/null +++ b/app/scripts/services/wcagSpecIdMap.js @@ -0,0 +1,341 @@ +'use strict'; + +angular + .module('wcagReporter') + /** + * wcagSpecIdMapping + * --- + * Necessary version management for WCAG#IDs + * Contains a 2-dimensional array, call it a tupple, + * of successcriterion id versions. + * Starting on index 0 with first id version WCAG 2.0 + * Callable like wcagSpecIdMap[SC][IDVersion]. + * + * Current versions: [ + * [0] => WCAG2.0, + * [1] => WCAG2.1 + * ] + * + * An empty ID version means the criterion did not exist for that version, + * e.g.: ['', 'orientation'] means the criterion id orientation + * was added since WCAG version 2.1 + * and ['text-equiv-all', 'non-text-content'] means the criterion id changed over time. + * --- + * @return Array[Array[...scId]] + */ + .constant('wcagSpecIdMap', [ + // ids here grouped sc per array + // From first to newest specification like: 2.0, 2.1, 2.2, 3.0? + [ + 'text-equiv-all', + 'non-text-content' + ], + [ + 'media-equiv-av-only-alt', + 'audio-only-and-video-only-prerecorded' + ], + [ + 'media-equiv-captions', + 'captions-prerecorded' + ], + [ + 'media-equiv-audio-desc', + 'audio-description-or-media-alternative-prerecorded' + ], + [ + 'media-equiv-real-time-captions', + 'captions-live' + ], + [ + 'media-equiv-audio-desc-only', + 'audio-description-prerecorded' + ], + [ + 'media-equiv-sign', + 'sign-language-prerecorded' + ], + [ + 'media-equiv-extended-ad', + 'extended-audio-description-prerecorded' + ], + [ + 'media-equiv-text-doc', + 'media-alternative-prerecorded' + ], + [ + 'media-equiv-live-audio-only', + 'audio-only-live' + ], + [ + 'content-structure-separation-programmatic', + 'info-and-relationships' + ], + [ + 'content-structure-separation-sequence', + 'meaningful-sequence' + ], + [ + 'content-structure-separation-understanding', + 'sensory-characteristics' + ], + [ + '', + 'orientation' + ], + [ + '', + 'identify-input-purpose' + ], + [ + '', + 'identify-purpose' + ], + [ + 'visual-audio-contrast-without-color', + 'use-of-color' + ], + [ + 'visual-audio-contrast-dis-audio', + 'audio-control' + ], + [ + 'visual-audio-contrast-contrast', + 'contrast-minimum' + ], + [ + 'visual-audio-contrast-scale', + 'resize-text' + ], + [ + 'visual-audio-contrast-text-presentation', + 'images-of-text' + ], + [ + 'visual-audio-contrast7', + 'contrast-enhanced' + ], + [ + 'visual-audio-contrast-noaudio', + 'low-or-no-background-audio' + ], + [ + 'visual-audio-contrast-visual-presentation', + 'visual-presentation' + ], + [ + 'visual-audio-contrast-text-images', + 'images-of-text-no-exception' + ], + [ + '', + 'reflow' + ], + [ + '', + 'non-text-contrast' + ], + [ + '', + 'text-spacing' + ], + [ + '', + 'content-on-hover-or-focus' + ], + [ + 'keyboard-operation-keyboard-operable', + 'keyboard' + ], + [ + 'keyboard-operation-trapping', + 'no-keyboard-trap' + ], + [ + 'keyboard-operation-all-funcs', + 'keyboard-no-exception' + ], + [ + '', + 'character-key-shortcuts' + ], + [ + 'time-limits-required-behaviors', + 'timing-adjustable' + ], + [ + 'time-limits-pause', + 'pause-stop-hide' + ], + [ + 'time-limits-no-exceptions', + 'no-timing' + ], + [ + 'time-limits-postponed', + 'interruptions' + ], + [ + 'time-limits-server-timeout', + 're-authenticating' + ], + [ + '', + 'timeouts' + ], + [ + 'seizure-does-not-violate', + 'three-flashes-or-below-threshold' + ], + [ + 'seizure-three-times', + 'three-flashes' + ], + [ + '', + 'animation-from-interactions' + ], + [ + 'navigation-mechanisms-skip', + 'bypass-blocks' + ], + [ + 'navigation-mechanisms-title', + 'page-titled' + ], + [ + 'navigation-mechanisms-focus-order', + 'focus-order' + ], + [ + 'navigation-mechanisms-refs', + 'link-purpose-in-context' + ], + [ + 'navigation-mechanisms-mult-loc', + 'multiple-ways' + ], + [ + 'navigation-mechanisms-descriptive', + 'headings-and-labels' + ], + [ + 'navigation-mechanisms-focus-visible', + 'focus-visible' + ], + [ + 'navigation-mechanisms-location', + 'location' + ], + [ + 'navigation-mechanisms-link', + 'link-purpose-link-only' + ], + [ + 'navigation-mechanisms-headings', + 'section-headings' + ], + [ + '', + 'pointer-gestures' + ], + [ + '', + 'pointer-cancellation' + ], + [ + '', + 'label-in-name' + ], + [ + '', + 'motion-actuation' + ], + [ + '', + 'target-size' + ], + [ + '', + 'concurrent-input-mechanisms' + ], + [ + 'meaning-doc-lang-id', + 'language-of-page' + ], + [ + 'meaning-other-lang-id', + 'language-of-parts' + ], + [ + 'meaning-idioms', + 'unusual-words' + ], + [ + 'meaning-located', + 'abbreviations' + ], + [ + 'meaning-supplements', + 'reading-level' + ], + [ + 'meaning-pronunciation', + 'pronunciation' + ], + [ + 'consistent-behavior-receive-focus', + 'on-focus' + ], + [ + 'consistent-behavior-unpredictable-change', + 'on-input' + ], + [ + 'consistent-behavior-consistent-locations', + 'consistent-navigation' + ], + [ + 'consistent-behavior-consistent-functionality', + 'consistent-identification' + ], + [ + 'consistent-behavior-no-extreme-changes-context', + 'change-on-request' + ], + [ + 'minimize-error-identified', + 'error-identification' + ], + [ + 'minimize-error-cues', + 'labels-or-instructions' + ], + [ + 'minimize-error-suggestions', + 'error-suggestion' + ], + [ + 'minimize-error-reversible', + 'error-prevention-legal-financial-data' + ], + [ + 'minimize-error-context-help', + 'help' + ], + [ + 'minimize-error-reversible-all', + 'error-prevention-all' + ], + [ + 'ensure-compat-parses', + 'parsing' + ], + [ + 'ensure-compat-rsv', + 'name-role-value' + ], + [ + '', + 'status-messages' + ] + ]); diff --git a/app/styles/bootstrap-extend.scss b/app/styles/bootstrap-extend.scss index 15056f7b..81d3ae62 100644 --- a/app/styles/bootstrap-extend.scss +++ b/app/styles/bootstrap-extend.scss @@ -36,7 +36,6 @@ body { .btn.btn-primary-invert { background-color: $body-bg; - padding: 5px 10px 4px; } .navbar-nav > li > a { diff --git a/app/styles/evaluate.scss b/app/styles/evaluate.scss index 19c57378..82ae37fb 100644 --- a/app/styles/evaluate.scss +++ b/app/styles/evaluate.scss @@ -104,7 +104,7 @@ h3 + .collapsing { .btn.btn-primary-invert { background: white; - border: 2px solid #2570b0; + border-color: #2570b0; color: #2570b0; &:focus, @@ -324,19 +324,35 @@ a.info-icon { border-radius: 3px; > legend { - display: inline-block; - float: left; - margin: 2px 10px; + margin: 0; border: 0; width: auto; } - > label.btn { - padding: 2px 7px 0; - line-height: 15px; + > fieldset + fieldset { + margin-top: 0.25em; + } + + & label.btn { + display: inline-flex; + align-items: center; + justify-content: space-between; - @include transition(200ms linear background); - @include transition(200ms linear color); + @include transition( + 200ms linear color, + 200ms linear background-color + ); + } + + & input { + margin: 0; + width: 1em; + height: 1em; + + & + span { + margin-left: 0.25em; + line-height: 1; + } } } diff --git a/app/styles/main.scss b/app/styles/main.scss index 02aa2941..b88632dc 100644 --- a/app/styles/main.scss +++ b/app/styles/main.scss @@ -1,6 +1,6 @@ $body-bg: #f5f5f5; $icon-font-path: 'bootstrap/'; -$wai-border-color: #930; +$wai-border-color: #005A9C; $panel-bg: $body-bg; $list-group-bg: $body-bg; $navbar-default-bg: #f2f2f2; @@ -8,7 +8,7 @@ $input-border: #aaa; $panel-default-heading-bg: #e8e8e8; $panel-default-border: #d6d6d6; $textcolor-light: #707070; -$brand-primary: #2570b0; +$brand-primary: #005A9C; $navbar-default-link-color: #6e6e6e; $font-size-base: 16px; $panel-body-padding: 7px 15px; @@ -93,16 +93,6 @@ select { .wert-head .navbar { min-height: auto; - - li > a { - padding-top: 9px; - padding-bottom: 9px; - } - } - - .branding img { - height: 38px; - width: auto; } } @@ -178,67 +168,27 @@ html.full-page { } } -@media (min-width: $screen-sm-max) { - - body .wert-head { - margin-bottom: 81px; - - & > .navbar { - position: fixed; - width: 100%; - top: 0; - z-index: 3; - } - } -} - body .wert-head { & > .navbar { + border: none; border-top: 5px solid $wai-border-color; - border-bottom: 0; -webkit-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); -moz-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); + border-radius: 0; } .branding { - .wai img { - margin: 1px 0; + > .nav { + display: flex; + background-color: #005A9C; } a { - display: inline-block; outline-color: $wai-border-color; } - - @media (max-width: $screen-xs-max) { - border-bottom: none; - } - - @media (max-width: $screen-sm-max) { - display: block; - border-bottom: 1px #ccc solid; - padding: 5px 15px; - - .navbar-nav.navbar-right:last-child { - float: none !important; - } - - .w3c img { - margin: -1px 0; - } - - .wai { - float: right; - - img { - @include border-right-radius(2px); - @include border-left-radius(2px); - } - } - } } .navbar-nav.navbar-right:last-child { diff --git a/app/views/directives/fullReport.html b/app/views/directives/fullReport.html index 1510acfc..ded2d998 100644 --- a/app/views/directives/fullReport.html +++ b/app/views/directives/fullReport.html @@ -30,17 +30,17 @@

{{'HTML_REPORT.HD_SPECIFICS' | translate }}

{{'HTML_REPORT.HD_DOCS' | translate }}

- \ No newline at end of file + diff --git a/app/views/directives/successCriterion.html b/app/views/directives/successCriterion.html index 7802de3f..f4475c42 100644 --- a/app/views/directives/successCriterion.html +++ b/app/views/directives/successCriterion.html @@ -12,7 +12,7 @@ {{'AUDIT.BTN_SHOW_TEXT' | translate}} - + \ No newline at end of file + diff --git a/app/views/error.html b/app/views/error.html index 6215de5d..d9f145be 100644 --- a/app/views/error.html +++ b/app/views/error.html @@ -8,7 +8,7 @@

{{ setTitle( translate('ERROR.TITLE') ) }}