diff --git a/.circleci/config.yml b/.circleci/config.yml index 0d470253e9c..87499e403ed 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -183,6 +183,7 @@ jobs: yarn run typecheck bundle exec rubocop bundle exec slim-lint app/views + make check_asset_strings build-latest-container: working_directory: ~/identity-idp docker: diff --git a/lib/asset_checker.rb b/lib/asset_checker.rb index 3893a4151cb..1efc6e9db24 100644 --- a/lib/asset_checker.rb +++ b/lib/asset_checker.rb @@ -1,13 +1,25 @@ +require 'yaml' + class AssetChecker - def self.check_files(argv) - assets_file = 'app/assets/javascripts/assets.js.erb' - translations_file = 'app/assets/javascripts/i18n-strings.js.erb' + ASSETS_FILE = 'app/assets/javascripts/assets.js.erb'.freeze + TRANSLATIONS_FILE = 'config/js_locale_strings.yml'.freeze + + attr_reader :files, :assets_file, :translations_file + + def initialize(files, assets_file: ASSETS_FILE, translations_file: TRANSLATIONS_FILE) + @files = files + @assets_file = assets_file + @translations_file = translations_file + end + + # @return [Boolean] true if any files are missing + def check_files @asset_strings = load_included_strings(assets_file) - @translation_strings = load_included_strings(translations_file) - argv.any? { |f| file_has_missing?(f) } + @translation_strings = YAML.load_file(translations_file) + files.any? { |f| file_has_missing?(f) } end - def self.file_has_missing?(file) + def file_has_missing?(file) data = File.open(file).read missing_translations = find_missing(data, /\Wt\s?\(['"]([^'"]*?)['"]\)/, @translation_strings) missing_assets = find_missing(data, /\WassetPath=["'](.*?)['"]/, @asset_strings) @@ -24,14 +36,14 @@ def self.file_has_missing?(file) has_missing end - def self.find_missing(file_data, pattern, source) + def find_missing(file_data, pattern, source) strings = (file_data.scan pattern).flatten strings.reject { |s| source.include? s } end - def self.load_included_strings(file) + def load_included_strings(file) data = File.open(file).read - key_data = data.split('<% keys = [')[1].split('] %>')[0] + key_data = data.split('<% keys = [').last.split('] %>').first key_data.scan(/['"](.*)['"]/).flatten end end diff --git a/scripts/check-assets b/scripts/check-assets index 196820f1391..984226f02ed 100755 --- a/scripts/check-assets +++ b/scripts/check-assets @@ -2,4 +2,6 @@ $LOAD_PATH.unshift(File.expand_path('../lib', File.dirname(__FILE__))) require 'asset_checker' -exit(AssetChecker.check_files(ARGV) ? 1 : 0) +has_missing = AssetChecker.new(ARGV).check_files + +exit 1 if has_missing diff --git a/spec/lib/asset_checker_spec.rb b/spec/lib/asset_checker_spec.rb index e82dc0c82ac..3c0732618ce 100644 --- a/spec/lib/asset_checker_spec.rb +++ b/spec/lib/asset_checker_spec.rb @@ -1,82 +1,117 @@ require 'asset_checker' -def get_js_with_strings(asset, translation) - " - import React from 'react'; - import AcuantCapture from './acuant-capture'; - import DocumentTips from './document-tips'; - import Image from './image'; - import useI18n from '../hooks/use-i18n'; - - function DocumentCapture() { - const { t } = useI18n(); - - const sample = ( - \"Sample - ); - - return ( - <> -

{t('#{translation}')}

- - - \"\" - - ); - } - - export default DocumentCapture; - " -end - RSpec.describe AssetChecker do - describe '.check_files' do + subject(:asset_checker) do + AssetChecker.new(files, assets_file: assets_file, translations_file: translations_file) + end + + describe '::ASSETS_FILE' do + it 'exists' do + expect(File.exist?(AssetChecker::ASSETS_FILE)).to eq(true) + end + end + + describe '::TRANSLATIONS_FILE' do + it 'exists' do + expect(File.exist?(AssetChecker::TRANSLATIONS_FILE)).to eq(true) + end + end + + describe '#check_files' do + let(:assets_file) { Tempfile.new } + let(:translations_file) { Tempfile.new } + + before do + File.open(assets_file.path, 'w') do |f| + f.puts <<-STR + <% keys = [ + #{asset_strings.map(&:inspect).join("\n")} + ] %> + STR + end + + File.open(translations_file.path, 'w') do |f| + f.puts YAML.dump(translation_strings) + end + end + + after do + assets_file.unlink + translations_file.unlink + end + let(:translation_strings) { %w[first_translation second_translation] } let(:asset_strings) { %w[first_asset.png second_asset.gif] } context 'with matching assets' do + let(:files) { [tempfile.path] } let(:tempfile) { Tempfile.new } before do File.open(tempfile.path, 'w') do |f| - f.puts(get_js_with_strings('first_asset.png', 'first_translation')) + f.puts(build_js_with_strings('first_asset.png', 'first_translation')) end end after { tempfile.unlink } it 'identifies no issues ' do - allow(AssetChecker).to receive(:load_included_strings).and_return(asset_strings, - translation_strings) - - expect(AssetChecker.check_files([tempfile.path])).to eq(false) + expect(asset_checker.check_files).to eq(false) end end context 'with an asset mismatch' do + let(:files) { [tempfile.path] } let(:tempfile) { Tempfile.new } before do File.open(tempfile.path, 'w') do |f| - f.puts(get_js_with_strings('wont_find.svg', 'not-found')) + f.puts(build_js_with_strings('wont_find.svg', 'not-found')) end end after { tempfile.unlink } it 'identifies issues' do - allow(AssetChecker).to receive(:load_included_strings).and_return(asset_strings, - translation_strings) - expect(AssetChecker).to receive(:warn).with(tempfile.path) - expect(AssetChecker).to receive(:warn).with('Missing translation, not-found') - expect(AssetChecker).to receive(:warn).twice.with('Missing asset, wont_find.svg') - expect(AssetChecker.check_files([tempfile.path])).to eq(true) + expect(asset_checker).to receive(:warn).with(tempfile.path) + expect(asset_checker).to receive(:warn).with('Missing translation, not-found') + expect(asset_checker).to receive(:warn).twice.with('Missing asset, wont_find.svg') + expect(asset_checker.check_files).to eq(true) end end end + + def build_js_with_strings(asset, translation) + <<-STR + import React from 'react'; + import AcuantCapture from './acuant-capture'; + import DocumentTips from './document-tips'; + import Image from './image'; + import useI18n from '../hooks/use-i18n'; + + function DocumentCapture() { + const { t } = useI18n(); + + const sample = ( + Sample front of state issued ID + ); + + return ( + <> +

{t('#{translation}')}

+ + + + + ); + } + + export default DocumentCapture; + STR + end end