Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 21 additions & 9 deletions lib/asset_checker.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
4 changes: 3 additions & 1 deletion scripts/check-assets
Original file line number Diff line number Diff line change
Expand Up @@ -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
129 changes: 82 additions & 47 deletions spec/lib/asset_checker_spec.rb
Original file line number Diff line number Diff line change
@@ -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 = (
<Image
assetPath=\"#{asset}\"
alt=\"Sample front of state issued ID\"
width={450}
height={338}
/>
);

return (
<>
<h2>{t('#{translation}')}</h2>
<DocumentTips sample={sample} />
<AcuantCapture />
<Image assetPath=\"#{asset}\" alt=\"\" />
</>
);
}

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 = (
<Image
assetPath="#{asset}"
alt="Sample front of state issued ID"
width={450}
height={338}
/>
);

return (
<>
<h2>{t('#{translation}')}</h2>
<DocumentTips sample={sample} />
<AcuantCapture />
<Image assetPath="#{asset}" alt="" />
</>
);
}

export default DocumentCapture;
STR
end
end