Files
Termind/Scripts/Tests/test_screenshots.rb
T
Kaede Akino a5b269ee20 feat(i18n): complete localized screenshots and language ordering
Localize German, French, and Korean demo content and capture workflows. Preserve real software keyboards and invariant numeric parsing during captures. Sort language choices by code and update the published documentation and marketing asset submodules.
2026-10-14 17:14:32 +08:00

547 lines
28 KiB
Ruby

require "minitest/autorun"
require "tmpdir"
require_relative "../../fastlane/lib/screenshots"
class ScreenshotsTests < Minitest::Test
def setup
@root = Dir.mktmpdir("termind-screenshots-")
FileUtils.mkdir_p(File.join(@root, "com.akinokaede.termind/assets"))
File.write(File.join(@root, "com.akinokaede.termind/.git"), "gitdir: fixture")
end
def teardown
FileUtils.remove_entry(@root)
end
def runner(platform = :ios, options = {}, environment = {})
Screenshots.new(platform, options, environment: environment, root: @root)
end
def test_default_capture_fails_before_build_when_submodule_is_missing
FileUtils.remove_entry(File.join(@root, "com.akinokaede.termind"))
error = assert_raises(RuntimeError) { runner.run { flunk "scan must not start" } }
assert_includes error.message, "git submodule update --init com.akinokaede.termind"
refute Dir.exist?(File.join(@root, ".build"))
refute Dir.exist?(File.join(@root, "com.akinokaede.termind"))
end
def test_explicit_capture_output_can_publish_without_submodule
FileUtils.remove_entry(File.join(@root, "com.akinokaede.termind"))
source = File.join(@root, "capture.png")
File.binwrite(source, "review capture")
capture = runner(:ios, output: "review")
capture.promote([[source, "en/ipad-15/vault.png"]])
assert_equal "review capture", File.binread(File.join(@root, "review/en/ipad-15/vault.png"))
refute Dir.exist?(File.join(@root, "com.akinokaede.termind"))
end
def test_lane_options_override_legacy_selectors_and_ipad_only_needs_no_iphone
capture = runner(:ios, { devices: "ipad", locales: "zh-Hant", scenes: "ssh,sftp" },
{ "TERMIND_SCREENSHOT_DEVICES" => "iphone", "TERMIND_SCREENSHOT_LOCALES" => "en" })
capture.define_singleton_method(:command) do |*args|
JSON.generate("devices" => { "com.apple.CoreSimulator.SimRuntime.iOS-27-0" => [
{ "name" => "iPad Pro 13-inch (M5)", "udid" => "ipad-id",
"deviceTypeIdentifier" => "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-12GB" }
] })
end
assert_equal ["ipad"], capture.find_devices.keys
assert_equal 2, capture.plan[:captures]
assert_equal ["zh-Hant"], capture.plan[:locales]
end
def test_runtime_defaults_follow_device_compatibility_and_allow_explicit_overrides
assert_equal "com.apple.CoreSimulator.SimRuntime.iOS-27-0", runner.plan[:runtime]
assert_equal "com.apple.CoreSimulator.SimRuntime.iOS-27-1", runner(:ios, devices: "iphone-duo-inner").plan[:runtime]
assert_equal "com.apple.CoreSimulator.SimRuntime.iOS-27-0",
runner(:ios, { devices: "iphone-duo-inner", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-27-0" }).plan[:runtime]
end
def test_a_renamed_pro_max_cannot_be_used_as_the_pro_capture_device
capture = runner(:ios, devices: "iphone")
capture.define_singleton_method(:command) do |*args|
JSON.generate("devices" => { "com.apple.CoreSimulator.SimRuntime.iOS-27-0" => [
{ "name" => "iPhone 18 Pro", "udid" => "wrong-model",
"deviceTypeIdentifier" => "com.apple.CoreSimulator.SimDeviceType.iPhone-18-Pro-Max" }
] })
end
assert_raises(RuntimeError) { capture.find_devices }
capture.define_singleton_method(:dimensions) { |_| [1320, 2868] }
assert_raises(RuntimeError) do
capture.validate_dimensions("pro-max.png", Screenshots::DEVICES.fetch("iphone").fetch(:dimensions))
end
end
def test_rejects_invalid_duplicate_and_empty_members_before_build
["ssh,ssh", "ssh,", "bogus"].each do |scenes|
assert_raises(ArgumentError) { runner(:ios, scenes: scenes) }
end
assert_raises(ArgumentError) { runner(:mac, scenes: "metrics-detail") }
assert_raises(ArgumentError) { runner(:ios, locales: "zh") }
assert_raises(ArgumentError) { runner(:ios, devices: "watch") }
assert_raises(ArgumentError) { runner(:ios, runtime: "com.apple.CoreSimulator.SimRuntime.iOS-26-0") }
end
def test_generated_xctestrun_preserves_host_settings_and_other_targets
capture = runner
fixture = { "EnvironmentVariables" => { "EXISTING" => "preserved" }, "TestHostPath" => "__TESTROOT__/App.app" }
document = { "TFITests" => fixture, "FrameworkTests" => { "CommandLineArguments" => ["unchanged"] } }
capture.configure_xctestrun(document, "zh-Hans", { "TERMIND_SCREENSHOTS" => "1" }, [])
assert_equal "preserved", fixture["EnvironmentVariables"]["EXISTING"]
assert_equal "__TESTROOT__/App.app", fixture["TestHostPath"]
assert_equal "CN", fixture["TestRegion"]
assert_equal ["-AppleLanguages", "(zh-Hans)", "-AppleLocale", "zh_CN"], fixture["CommandLineArguments"]
assert_equal ["unchanged"], document["FrameworkTests"]["CommandLineArguments"]
end
def test_japanese_capture_configures_japan_region_and_locale
capture = runner(:ios, locales: "ja")
target = {}
capture.configure_xctestrun({ "TFITests" => target }, "ja", {}, [])
assert_equal "ja", target["TestLanguage"]
assert_equal "JP", target["TestRegion"]
assert_equal ["-AppleLanguages", "(ja)", "-AppleLocale", "ja_JP"], target["CommandLineArguments"]
assert ScreenshotFrames.new({ locales: "ja" }, root: @root).plan.all? { |job| job[:output].include?("/ja/") }
end
def test_additional_marketing_locales_configure_native_capture_and_frame_paths
{ "ko" => ["KR", "ko_KR"], "de" => ["DE", "de_DE"], "fr" => ["FR", "fr_FR"] }.each do |language, (region, locale)|
%i[mac ios].each do |platform|
capture = runner(platform, locales: language)
target = {}
key = platform == :mac ? "TFMTests" : "TFITests"
capture.configure_xctestrun({ key => target }, language, {}, [])
assert_equal language, target["TestLanguage"]
assert_equal region, target["TestRegion"]
assert_equal "C", target["EnvironmentVariables"]["LC_NUMERIC"]
assert_equal ["-AppleLanguages", "(#{language})", "-AppleLocale", locale], target["CommandLineArguments"]
end
jobs = ScreenshotFrames.new({ locales: language }, root: @root).plan
assert jobs.all? { |job| job[:input].include?("/#{language}/") && job[:output].include?("/#{language}/") }
end
end
def test_test_plan_format_updates_only_the_capture_target
capture = runner(:mac)
target = { "BlueprintName" => "TFMTests" }
other = { "BlueprintName" => "FrameworkTests" }
document = { "TestConfigurations" => [{ "TestTargets" => [target, other] }] }
capture.configure_xctestrun(document, "zh-Hant", {}, ["--termind-demo=hosts:zh-Hant"])
assert_equal "TW", target["TestRegion"]
assert_equal "--termind-demo=hosts:zh-Hant", target["CommandLineArguments"].first
refute other.key?("TestLanguage")
end
def test_scan_configuration_pins_dependencies_and_serializes_hosted_tests
options = runner.scan_options("id=ipad", "vault-en")
assert_includes options[:xcargs], "-onlyUsePackageVersionsFromResolvedFile"
assert options[:skip_package_dependencies_resolution]
assert options[:disable_package_automatic_updates]
refute options[:parallel_testing]
assert options[:disable_concurrent_testing]
assert_equal 0, options[:number_of_retries]
end
def test_successful_capture_backs_up_original_bytes_and_manifest
capture = runner
original = File.join(@root, "com.akinokaede.termind/assets/captures/en/ipad-15/vault.png")
FileUtils.mkdir_p(File.dirname(original))
File.binwrite(original, "old image bytes")
source = File.join(@root, "new.png")
File.binwrite(source, "new image bytes")
capture.promote([[source, "en/ipad-15/vault.png"]])
assert_equal "new image bytes", File.binread(original)
backup = Dir.glob(File.join(@root, ".build/screenshot-backups/*/en/ipad-15/vault.png")).fetch(0)
assert_equal "old image bytes", File.binread(backup)
manifest = File.read(File.join(File.dirname(File.dirname(File.dirname(backup))), "SHA256SUMS"))
assert_equal "#{Digest::SHA256.hexdigest('old image bytes')} en/ipad-15/vault.png\n", manifest
end
def test_mac_captures_publish_localized_underscore_filenames
{ "command" => "command_generator", "metrics" => "metrics_overview" }.each do |scene, filename|
capture = runner(:mac, scenes: scene, locales: "zh-Hant")
capture.define_singleton_method(:test_configuration) { |*| File.join(@root, "test.xctestrun") }
capture.define_singleton_method(:capture_during_test) do |options, &block|
log = File.join(options.fetch(:buildlog_path), "Termind-TFM.log")
FileUtils.mkdir_p(File.dirname(log))
File.write(log, "TERMIND_MAC_SCREENSHOT_READY #{scene}\tzh-Hant\t123\ttoken\tactive\tkey\t2360\t1480\n")
block.call
end
capture.define_singleton_method(:command) do |*args|
raise "Unexpected command" unless args.first == "screencapture"
FileUtils.mkdir_p(File.dirname(args.last))
File.binwrite(args.last, "captured #{scene}")
end
capture.define_singleton_method(:dimensions) { |_| [2360, 1480] }
capture.capture_mac(scene, "zh-Hant", "platform=macOS")
destination = File.join(@root, "com.akinokaede.termind/assets/captures/zh-hant/mac-14/#{filename}.png")
assert_equal "captured #{scene}", File.binread(destination)
end
end
def test_mac_camera_strip_is_padded_without_stretching_full_screen_capture
capture = runner(:mac, scenes: "workspace", locales: "en")
capture.define_singleton_method(:test_configuration) { |*| File.join(@root, "test.xctestrun") }
capture.define_singleton_method(:capture_during_test) do |options, &block|
log = File.join(options.fetch(:buildlog_path), "Termind-TFM.log")
FileUtils.mkdir_p(File.dirname(log))
File.write(log, "TERMIND_MAC_SCREENSHOT_READY workspace\ten\t123\ttoken\tactive\tkey\t3024\t1964\n")
block.call
end
calls = []
capture.define_singleton_method(:command) do |*args|
calls << args
if args.first == "screencapture"
FileUtils.mkdir_p(File.dirname(args.last))
File.binwrite(args.last, "full-screen window")
end
end
sizes = [[3024, 1890], [3024, 1964]]
capture.define_singleton_method(:dimensions) { |_| sizes.shift }
capture.capture_mac("workspace", "en", "platform=macOS")
padding = calls.find { |args| args.first == "magick" }
assert_includes padding, "3024x1964"
assert_includes padding, "south"
refute_includes padding, "-resize"
assert_equal ["screencapture", "-l", "123", "-o"], calls.first.first(4)
end
def test_mobile_capture_rediscovers_container_after_host_reinstallation
%w[iphone ipad iphone-duo-outer iphone-duo-inner].each do |device|
assert_mobile_capture_rediscovers_container(device)
end
end
def assert_mobile_capture_rediscovers_container(device)
capture = runner(:ios, devices: device, scenes: "vault", locales: "zh-Hans")
old_container = File.join(@root, "old-container")
new_container = File.join(@root, "new-container")
control = File.join(new_container, "Documents", "screenshot-capture-control")
token = "11111111-1111-1111-1111-111111111111"
capture.define_singleton_method(:test_configuration) do |_, _, environment|
FileUtils.mkdir_p(control)
File.write(File.join(control, "ready"), ["vault", environment.fetch("TERMIND_SCREENSHOT_DEVICE"), "zh-hans", token,
environment.fetch("TERMIND_SCREENSHOT_CAPTURE_ID")].join("\t"))
File.join(@root, "fixture.xctestrun")
end
capture.define_singleton_method(:capture_during_test) { |_, &block| 2.times { block.call } }
commands = []
capture.define_singleton_method(:sleep) { |_| }
capture.define_singleton_method(:command) do |*args|
commands << args
if args.include?("displays")
next JSON.generate("result" => { "displays" => [
{ "active" => true, "nativeSize" => Screenshots::DEVICES.fetch(device).fetch(:dimensions).sort,
"uniqueId" => "display-#{device}" }
] })
end
if args.include?("screenshot")
FileUtils.mkdir_p(File.dirname(args.last))
File.binwrite(args.last, "new container capture")
end
""
end
capture.define_singleton_method(:dimensions) { |_| Screenshots::DEVICES.fetch(device).fetch(:dimensions) }
containers = [old_container, new_container]
status = Object.new
status.define_singleton_method(:success?) { true }
responses = lambda do |*args|
[args.include?("get_app_container") ? containers.shift : "", status]
end
Open3.stub(:capture2e, responses) do
capture.capture_mobile(device, { "udid" => "fixture", "state" => "Booted" }, "zh-Hans")
end
folder = { "iphone" => "iphone-6.3", "ipad" => "ipad-15" }.fetch(device, device)
destination = File.join(@root, "com.akinokaede.termind/assets/captures/zh-hans/#{folder}/vault.png")
assert_equal "new container capture", File.binread(destination)
assert File.exist?(File.join(control, "ack-#{token}"))
refute Dir.exist?(old_container)
status_bar = commands.find { |args| args.include?("override") }
assert_equal "discharging", status_bar[status_bar.index("--batteryState") + 1]
assert_equal "100", status_bar[status_bar.index("--batteryLevel") + 1]
if device == "iphone-duo-outer"
assert_includes commands.find { |args| args.include?("screenshot") }, "--display=primary"
elsif device == "iphone-duo-inner"
screenshot = commands.find { |args| args.include?("screenshot") }
assert_includes screenshot, "--display-unique-id"
assert_includes screenshot, "display-iphone-duo-inner"
end
FileUtils.remove_entry(new_container)
end
def test_duo_rejects_the_inactive_display_before_starting_capture
capture = runner(:ios, devices: "iphone-duo-inner")
capture.define_singleton_method(:command) do |*args|
JSON.generate("result" => { "displays" => [
{ "active" => true, "nativeSize" => [1398, 2034] },
{ "active" => false, "nativeSize" => [2007, 2853] }
] })
end
error = assert_raises(RuntimeError) { capture.validate_display("iphone-duo-inner", "duo-id") }
assert_includes error.message, "Select Open"
end
def test_failed_scan_never_promotes_a_staged_mac_capture
capture = runner(:mac, scenes: "hosts", locales: "en")
capture.define_singleton_method(:test_configuration) { |*| File.join(@root, "test.xctestrun") }
capture.instance_variable_set(:@scan, ->(_) { raise "scan failed" })
error = assert_raises(RuntimeError) { capture.capture_mac("hosts", "en", "platform=macOS") }
assert_equal "scan failed", error.message
refute Dir.exist?(File.join(@root, "com.akinokaede.termind/assets/captures"))
end
def test_capture_failure_is_reported_even_if_scan_succeeds
capture = runner
attempted = Queue.new
capture.instance_variable_set(:@scan, ->(_) { attempted.pop })
error = assert_raises(RuntimeError) do
capture.capture_during_test({}) do
attempted << true
raise "native capture failed"
end
end
assert_equal "native capture failed", error.message
end
end
require "minitest/mock"
require "fastlane"
require "frameit"
require "zlib"
require_relative "../../fastlane/lib/screenshot_frames"
class ScreenshotFramesTests < Minitest::Test
def setup
@root = Dir.mktmpdir("termind-frames-")
FileUtils.mkdir_p(File.join(@root, "com.akinokaede.termind/assets"))
File.write(File.join(@root, "com.akinokaede.termind/.git"), "gitdir: fixture")
FileUtils.mkdir_p(File.join(@root, "fastlane"))
FileUtils.cp(File.expand_path("../../fastlane/Framefile.json", __dir__), File.join(@root, "fastlane"))
frame_template(File.join(@root, "com.akinokaede.termind/assets/device_templates/iphone-18-pro.png"), 1350, 2760)
@templates = File.join(@root, "templates")
FileUtils.mkdir_p(@templates)
end
def teardown
FileUtils.remove_entry(@root)
end
def png(path, width, height)
chunk = lambda do |name, bytes|
[bytes.bytesize].pack("N") + name + bytes + [Zlib.crc32(name + bytes)].pack("N")
end
data = "\x89PNG\r\n\x1a\n".b + chunk.call("IHDR", [width, height, 8, 2, 0, 0, 0].pack("NNCCCCC"))
data += chunk.call("IDAT", Zlib.deflate(("\0" * (width * 3 + 1)) * height)) + chunk.call("IEND", "")
FileUtils.mkdir_p(File.dirname(path))
File.binwrite(path, data)
end
def frame_template(path, width, height)
FileUtils.mkdir_p(File.dirname(path))
ScreenshotFrames.new.command("magick", "-size", "#{width}x#{height}", "xc:none",
"-fill", "none", "-stroke", "black", "-strokewidth", "40",
"-draw", "roundrectangle 40,40,#{width - 41},#{height - 41},100,100", path)
end
def test_screen_mask_removes_exterior_corners_without_fading_content
runner = ScreenshotFrames.new
template = File.join(@root, "rounded-frame.png")
runner.command("magick", "-size", "120x180", "xc:none", "-fill", "none", "-stroke", "black",
"-strokewidth", "8", "-draw", "roundrectangle 8,8,111,171,25,25",
"-fill", "black", "-stroke", "none", "-draw", "rectangle 50,8,70,28", template)
[0, -90, 90].each do |rotation|
landscape = !rotation.zero?
source = File.join(@root, "screen.png")
runner.command("magick", "-size", landscape ? "160x100" : "100x160", "xc:#327abe", source)
runner.mask_screen(source, template, "+10+10", rotation: rotation)
assert_equal(landscape ? [160, 100] : [100, 160], FastImage.size(source))
center = landscape ? "80,50" : "50,80"
assert_equal "1", runner.command("magick", source, "-format", "%[fx:p{#{center}}.a]", "info:").strip
assert_equal "0", runner.command("magick", source, "-format", "%[fx:p{0,0}.a]", "info:").strip
assert_equal "50,122,190", runner.command("magick", source, "-format",
"%[fx:round(255*p{#{center}}.r)],%[fx:round(255*p{#{center}}.g)],%[fx:round(255*p{#{center}}.b)]", "info:").strip
notch = { 0 => "50,5", -90 => "5,50", 90 => "155,50" }.fetch(rotation)
assert_equal "0", runner.command("magick", source, "-format", "%[fx:p{#{notch}}.a]", "info:").strip
end
end
def test_screen_mask_rejects_an_open_bezel
runner = ScreenshotFrames.new
template = File.join(@root, "open-frame.png")
source = File.join(@root, "screen.png")
runner.command("magick", "-size", "120x180", "xc:none", template)
png(source, 100, 160)
original = Digest::SHA256.file(source).hexdigest
assert_raises(RuntimeError) { runner.mask_screen(source, template, "+10+10") }
assert_equal original, Digest::SHA256.file(source).hexdigest
end
def test_mac_composition_clips_corners_and_preserves_source_and_content
template = File.join(@root, "mac-frame.png")
runner = ScreenshotFrames.new({ mac_frame: template }, root: @root)
runner.command("magick", "-size", "3860x2320", "xc:none", "-fill", "none",
"-stroke", "black", "-strokewidth", "20",
"-draw", "roundrectangle 408,58,3451,2041,100,100", template)
source = File.join(@root, "capture.png")
output = File.join(@root, "composed.png")
runner.command("magick", "-size", "3024x1964", "xc:#327abe", "-fill", "red",
"-draw", "point 1512,982", source)
original = Digest::SHA256.file(source).hexdigest
runner.compose_mac(source, output)
assert_equal [3860, 2320], FastImage.size(output)
# Native screen pixels must land at the aperture offset without resampling.
assert_equal "255,0,0", runner.command("magick", output, "-format",
"%[fx:round(255*p{1930,1050}.r)],%[fx:round(255*p{1930,1050}.g)],%[fx:round(255*p{1930,1050}.b)]", "info:").strip
assert_equal original, Digest::SHA256.file(source).hexdigest
# The old rectangular placement leaked here, outside the curved bezel.
assert_equal "0", runner.command("magick", output, "-format", "%[fx:p{418,68}.a]", "info:").strip
assert_equal "1", runner.command("magick", output, "-format", "%[fx:p{1930,1160}.a]", "info:").strip
assert_equal "50,122,190", runner.command("magick", output, "-format",
"%[fx:round(255*p{1930,1160}.r)],%[fx:round(255*p{1930,1160}.g)],%[fx:round(255*p{1930,1160}.b)]", "info:").strip
end
def test_default_frames_only_include_website_used_scenes
jobs = ScreenshotFrames.new({}, root: @root).plan
assert_equal 7 * Screenshots::LOCALES.size, jobs.size
refute jobs.any? { |job| job[:input].end_with?("/ipad-15/ssh.png", "/iphone-6.3/metrics.png") }
explicit = ScreenshotFrames.new({ devices: "ipad", scenes: "ssh", locales: "en" }, root: @root).plan
assert_equal 1, explicit.size
assert explicit.first[:input].end_with?("/ipad-15/ssh.png")
end
def test_default_frames_fail_before_creating_outputs_when_submodule_is_missing
FileUtils.remove_entry(File.join(@root, "com.akinokaede.termind"))
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en" }, root: @root)
error = assert_raises(RuntimeError) { runner.run { flunk "frameit must not start" } }
assert_includes error.message, "git submodule update --init com.akinokaede.termind"
refute Dir.exist?(File.join(@root, "com.akinokaede.termind"))
end
def test_explicit_frame_paths_work_without_submodule_and_keep_source_bytes
template = File.join(@root, "external-template.png")
FileUtils.mv(File.join(@root, "com.akinokaede.termind/assets/device_templates/iphone-18-pro.png"), template)
FileUtils.remove_entry(File.join(@root, "com.akinokaede.termind"))
source = File.join(@root, "review-input/en/iphone-6.3/vault.png")
png(source, 1206, 2622)
original_hash = Digest::SHA256.file(source).hexdigest
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en",
input: "review-input", output: "review-output", iphone_frame: template }, root: @root)
downloader = Object.new
downloader.define_singleton_method(:frames_exist?) { true }
Frameit::FrameDownloader.stub(:templates_path, @templates) do
Frameit::FrameDownloader.stub(:new, downloader) do
runner.run do |options|
assert_equal "iPhone 18 Pro", options.fetch(:force_device_type)
framefile = JSON.parse(File.read(File.join(options.fetch(:path), "Framefile.json")))
assert_equal({ "offset" => "+72+69", "width" => 1206 }, framefile.fetch("default").fetch("offset"))
png(File.join(options.fetch(:path), "iphone_framed.png"), 1350, 2760)
end
end
end
assert File.exist?(File.join(@root, "review-output/en/iphone-6.3/vault.png"))
assert_equal original_hash, Digest::SHA256.file(source).hexdigest
refute Dir.exist?(File.join(@root, "com.akinokaede.termind"))
end
def test_default_template_works_without_a_local_build_cache
source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone-6.3/vault.png")
png(source, 1206, 2622)
refute Dir.exist?(File.join(@root, ".build"))
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en" }, root: @root)
downloader = Object.new
downloader.define_singleton_method(:frames_exist?) { true }
Frameit::FrameDownloader.stub(:templates_path, @templates) do
Frameit::FrameDownloader.stub(:new, downloader) do
runner.run do |options|
installed = File.join(@templates, "Apple iPhone 18 Pro Silver.png")
assert_equal Digest::SHA256.file(File.join(@root, "com.akinokaede.termind/assets/device_templates/iphone-18-pro.png")).hexdigest,
Digest::SHA256.file(installed).hexdigest
png(File.join(options.fetch(:path), "iphone_framed.png"), 1350, 2760)
end
end
end
assert_equal [1350, 2760], FastImage.size(File.join(@root, "com.akinokaede.termind/assets/device_frames/en/iphone-6.3/vault.png"))
end
def test_rejects_pro_max_sources_before_calling_frameit
source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone-6.3/vault.png")
png(source, 1320, 2868)
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en" }, root: @root)
called = false
assert_raises(RuntimeError) { runner.run { called = true } }
refute called
refute Dir.exist?(File.join(@root, "com.akinokaede.termind/assets/device_frames"))
end
def test_duo_frames_preserve_native_sources_and_use_each_display_aperture
templates = File.join(@root, "templates")
FileUtils.mkdir_p(templates)
downloader = Object.new
downloader.define_singleton_method(:frames_exist?) { true }
expected = {
"outer" => [[1398, 2034], [1574, 2194], "+88+80", "iPhone Duo Outer"],
"inner" => [[2853, 2007], [2247, 3093], "+120+120", "iPhone Duo Inner"]
}
expected.each do |display, (native, frame, offset, name)|
device = "iphone-duo-#{display}"
source = File.join(@root, "input/en/#{device}/vault.png")
template = File.join(@root, "#{device}.png")
png(source, *native)
frame_template(template, *frame)
source_hash = Digest::SHA256.file(source).hexdigest
options = { devices: device, scenes: "vault", locales: "en", input: "input", output: "output",
"iphone_duo_#{display}_frame".to_sym => template }
Frameit::FrameDownloader.stub(:templates_path, templates) do
Frameit::FrameDownloader.stub(:new, downloader) do
ScreenshotFrames.new(options, root: @root).run do |configuration|
assert_equal name, configuration.fetch(:force_device_type)
framefile = JSON.parse(File.read(File.join(configuration.fetch(:path), "Framefile.json")))
assert_equal({ "offset" => offset, "width" => native.min }, framefile.fetch("default").fetch("offset"))
assert_equal :landscape_right, configuration.fetch(:force_orientation_block).call(nil) if display == "inner"
png(File.join(configuration.fetch(:path), "#{device}_framed.png"), *(display == "inner" ? frame.reverse : frame))
end
end
end
assert_equal source_hash, Digest::SHA256.file(source).hexdigest
assert_equal(display == "inner" ? frame.reverse : frame, FastImage.size(File.join(@root, "output/en/#{device}/vault.png")))
end
end
def test_missing_duo_template_fails_before_publishing
runner = ScreenshotFrames.new({ devices: "iphone-duo-inner", locales: "en" }, root: @root)
error = assert_raises(RuntimeError) { runner.run { flunk "frameit must not start" } }
assert_includes error.message, "iphone_duo_inner_frame:"
assert_includes error.message, "git submodule update --init com.akinokaede.termind"
refute Dir.exist?(File.join(@root, "com.akinokaede.termind/assets/device_frames"))
end
def test_missing_frameit_output_keeps_all_existing_outputs_and_sources
%w[en zh-hans].each do |locale|
png(File.join(@root, "com.akinokaede.termind/assets/captures/#{locale}/iphone-6.3/vault.png"), 1206, 2622)
end
destination = File.join(@root, "com.akinokaede.termind/assets/device_frames/en/iphone-6.3/vault.png")
FileUtils.mkdir_p(File.dirname(destination))
File.write(destination, "existing frame")
source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone-6.3/vault.png")
original_hash = Digest::SHA256.file(source).hexdigest
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en,zh-Hans" }, root: @root)
downloader = Object.new
downloader.define_singleton_method(:frames_exist?) { true }
calls = 0
Frameit::FrameDownloader.stub(:templates_path, @templates) do
Frameit::FrameDownloader.stub(:new, downloader) do
assert_raises(RuntimeError) do
runner.run do |options|
calls += 1
png(File.join(options.fetch(:path), "iphone_framed.png"), 1350, 2760) if calls == 1
# frameit can log an error and return successfully without an image.
end
end
end
end
assert_equal 2, calls
assert_equal "existing frame", File.read(destination)
assert_equal original_hash, Digest::SHA256.file(source).hexdigest
refute File.exist?(destination.sub("/en/", "/zh-hans/"))
end
end