diff --git a/.github/workflows/app-discovery-qa.yml b/.github/workflows/app-discovery-qa.yml
index 164c95c0..d115a7d2 100644
--- a/.github/workflows/app-discovery-qa.yml
+++ b/.github/workflows/app-discovery-qa.yml
@@ -10,6 +10,8 @@ on:
- support/twotaku/index.html
- support/debate-me/index.html
- support/idea-market/index.html
+ - support/blur/index.html
+ - tests/blur-support-growth.test.mjs
- tests/idea-market-support-growth.test.mjs
- tests/debate-me-support-growth.test.mjs
- tests/dotchi-support-growth.test.mjs
@@ -36,6 +38,8 @@ on:
- support/twotaku/index.html
- support/debate-me/index.html
- support/idea-market/index.html
+ - support/blur/index.html
+ - tests/blur-support-growth.test.mjs
- tests/idea-market-support-growth.test.mjs
- tests/debate-me-support-growth.test.mjs
- tests/dotchi-support-growth.test.mjs
@@ -71,4 +75,4 @@ jobs:
- name: Verify non-destructive release scripts have valid shell syntax
run: bash -n scripts/publish_app_discovery.sh scripts/check_app_discovery_live.sh
- name: Verify search, referral links and guide listings
- run: node --test tests/catalog-growth.test.mjs tests/search-landing-growth.test.mjs tests/dotchi-support-growth.test.mjs tests/debate-me-support-growth.test.mjs tests/idea-market-support-growth.test.mjs
+ run: node --test tests/catalog-growth.test.mjs tests/search-landing-growth.test.mjs tests/dotchi-support-growth.test.mjs tests/debate-me-support-growth.test.mjs tests/idea-market-support-growth.test.mjs tests/blur-support-growth.test.mjs
diff --git a/scripts/check_app_discovery_live.sh b/scripts/check_app_discovery_live.sh
index a6cd3641..4e3fa16f 100644
--- a/scripts/check_app_discovery_live.sh
+++ b/scripts/check_app_discovery_live.sh
@@ -60,6 +60,14 @@ grep -Fq 'support_idea_market_hero' "$tmp/idea-market.html" || { echo 'FAIL Idea
if grep -Eq 'voice into a personal diary|Voice-to-text transcription|AI Transcription' "$tmp/idea-market.html"; then
echo 'FAIL Idea Market page still describes a voice diary' >&2; exit 1
fi
+# Blur listing must show a real single-player image-reveal experience.
+fetch '/support/blur/' 'blur.html'
+grep -Fq '
A fun image guessing game where pictures gradually become clearer. Can you guess before the reveal?
+
Key Features
-
Multiple difficulty levels
+
Daily blurred-image guessing challenge Reveal more of the picture step by step Guess before the full image appears Keep playing in infinite mode
Download
Available on Android. Download for free from Google Play and start using today.
@@ -183,7 +184,7 @@
Privacy & Data
const _oA=applyLang;applyLang=function(l){_oA(l);const t=APP_T[l]||APP_T.en;const h=document.getElementById('heroDesc');if(h&&t)h.textContent=t;};
(function(){const l=localStorage.getItem('sk_lang');const lang=l||detectLang();const t=APP_T[lang]||APP_T.en;const h=document.getElementById('heroDesc');if(h&&t)h.textContent=t;})();
-const FEAT_T={en:{0:"Each image starts with maximum blur",1:"Tap \"Reveal\" to reduce the blur",2:"Press \"I Know!\" when you recognize",3:"Score \"Got it [Correct]\" or \"Missed",4:"The fewer reveals you use",5:"Daily Challenge"},ja:{0:"遊び方",1:"画像は最大限のぼかしからスタート",2:"「Reveal」をタップするたびにぼかしが軽減",3:"画像がわかったら「I Know!」を押す",4:"タップ回数が少ないほど高スコア!",5:"特徴"},zh:{0:"自然与风景",1:"动物",2:"食物与饮品",3:"建筑与城市",4:"名人",5:"物品与静物"},zh_TW:{0:"自然與風景",1:"動物",2:"食物與飲品",3:"建築與城市",4:"名人",5:"物品與靜物"},ko:{0:"자연과 풍경",1:"동물",2:"음식과 음료",3:"건축과 도시",4:"유명인",5:"사물과 정물"},es:{0:"Naturaleza y Paisajes",1:"Animales",2:"Comida y Bebida",3:"Arquitectura y Ciudades",4:"Personas Famosas",5:"Objetos y Naturaleza Muerta"},pt:{0:"Natureza e Paisagens",1:"Animais",2:"Comida e Bebida",3:"Arquitetura e Cidades",4:"Pessoas Famosas",5:"Objetos e Natureza Morta"},fr:{0:"Nature et Paysages",1:"Animaux",2:"Nourriture et Boissons",3:"Architecture et Villes",4:"Célébrités",5:"Objets et Natures Mortes"},de:{0:"Natur und Landschaften",1:"Tiere",2:"Essen und Trinken",3:"Architektur und Städte",4:"Berühmte Persönlichkeiten",5:"Objekte und Stillleben"},it:{0:"Natura e Paesaggi",1:"Animali",2:"Cibo e Bevande",3:"Architettura e Città",4:"Persone Famose",5:"Oggetti e Nature Morte"},ru:{0:"Природа и Пейзажи",1:"Животные",2:"Еда и Напитки",3:"Архитектура и Города",4:"Известные Личности",5:"Объекты и Натюрморты"},ar:{0:"الطبيعة والمناظر الطبيعية",1:"الحيوانات",2:"الطعام والشراب",3:"العمارة والمدن",4:"المشاهير",5:"الأشياء والطبيعة الصامتة"},th:{0:"ธรรมชาติและภูมิทัศน์",1:"สัตว์",2:"อาหารและเครื่องดื่ม",3:"สถาปัตยกรรมและเมือง",4:"บุคคลที่มีชื่อเสียง",5:"วัตถุและภาพนิ่ง"},id:{0:"Alam dan Pemandangan",1:"Hewan",2:"Makanan dan Minuman",3:"Arsitektur dan Kota",4:"Orang Terkenal",5:"Benda dan Alam Benda"},tr:{0:"Doğa ve Manzaralar",1:"Hayvanlar",2:"Yiyecek ve İçecek",3:"Mimari ve Şehirler",4:"Ünlü İnsanlar",5:"Nesneler ve Natürmort"},nl:{0:"Natuur en Landschappen",1:"Dieren",2:"Eten en Drinken",3:"Architectuur en Steden",4:"Bekende Personen",5:"Voorwerpen en Stillevens"},vi:{0:"Thiên nhiên và Phong cảnh",1:"Động vật",2:"Thức ăn và Đồ uống",3:"Kiến trúc và Thành phố",4:"Người nổi tiếng",5:"Đồ vật và Tĩnh vật"}};
+const FEAT_T={en:{0:"Play the daily blurred-image puzzle",1:"Reveal the picture step by step",2:"Guess before the full reveal",3:"Keep guessing in infinite mode"},ja:{0:"毎日のぼかし画像クイズに挑戦",1:"画像を少しずつ鮮明にする",2:"全部見える前に答えを予想",3:"無限モードでも遊べる"},zh:{0:"挑战每日模糊图片谜题",1:"逐步揭示图片内容",2:"在完全显示前猜出答案",3:"在无限模式继续挑战"},zh_TW:{0:"挑戰每日模糊圖片謎題",1:"逐步揭示圖片內容",2:"在完全顯示前猜出答案",3:"在無限模式繼續挑戰"},ko:{0:"매일 흐릿한 이미지 퀴즈 도전",1:"사진을 단계별로 선명하게 보기",2:"완전히 공개되기 전에 정답 맞히기",3:"무한 모드로 계속 플레이"},es:{0:"Resuelve el acertijo diario de imagen borrosa",1:"Revela la imagen paso a paso",2:"Adivina antes de descubrirla por completo",3:"Sigue jugando en modo infinito"},pt:{0:"Resolva o desafio diário de imagem borrada",1:"Revele a imagem passo a passo",2:"Adivinhe antes da revelação completa",3:"Continue no modo infinito"},fr:{0:"Relevez le défi quotidien de l'image floue",1:"Révélez l'image progressivement",2:"Devinez avant la révélation complète",3:"Continuez en mode infini"},de:{0:"Löse das tägliche Rätsel mit verschwommenem Bild",1:"Enthülle das Bild Schritt für Schritt",2:"Rate vor der vollständigen Auflösung",3:"Spiele im Endlosmodus weiter"},it:{0:"Gioca all'indovinello giornaliero con immagini sfocate",1:"Rivela l'immagine poco alla volta",2:"Indovina prima che sia del tutto visibile",3:"Continua nella modalità infinita"},ru:{0:"Решайте ежедневную загадку с размытым фото",1:"Открывайте изображение постепенно",2:"Угадайте до полного раскрытия",3:"Продолжайте в бесконечном режиме"},ar:{0:"حل لغز الصورة الضبابية اليومي",1:"اكشف الصورة تدريجيًا",2:"خمّن قبل ظهور الصورة بالكامل",3:"واصل اللعب في الوضع اللانهائي"},th:{0:"ทายภาพเบลอประจำวัน",1:"ค่อย ๆ เปิดเผยภาพทีละขั้น",2:"ทายให้ได้ก่อนเห็นภาพทั้งหมด",3:"เล่นต่อได้ในโหมดไม่สิ้นสุด"},id:{0:"Mainkan teka-teki gambar buram harian",1:"Ungkap gambar sedikit demi sedikit",2:"Tebak sebelum gambar terlihat penuh",3:"Lanjutkan bermain dalam mode tanpa akhir"},tr:{0:"Günlük bulanık resim bulmacasını çözün",1:"Resmi adım adım açın",2:"Tamamen görünmeden önce tahmin edin",3:"Sonsuz modda oynamaya devam edin"},nl:{0:"Speel de dagelijkse wazige-fotoquiz",1:"Onthul de foto stap voor stap",2:"Raad voordat het beeld helemaal zichtbaar is",3:"Speel verder in de eindeloze modus"},vi:{0:"Chơi thử thách ảnh mờ mỗi ngày",1:"Mở dần hình ảnh từng bước",2:"Đoán trước khi ảnh hiện rõ hoàn toàn",3:"Tiếp tục chơi ở chế độ vô tận"}};
const __oF=applyLang;applyLang=function(l){__oF(l);var ft=FEAT_T[l]||FEAT_T.en;var g=document.querySelector('.features-grid');if(ft&&g){g.innerHTML='';Object.keys(ft).sort(function(a,b){return a-b;}).forEach(function(k){if(ft[k]){var d=document.createElement('div');d.className='feature';d.innerHTML='
'+ft[k]+'';g.appendChild(d);}});}};
(function(){var l=localStorage.getItem('sk_lang')||detectLang();var ft=FEAT_T[l]||FEAT_T.en;var g=document.querySelector('.features-grid');if(ft&&g){g.innerHTML='';Object.keys(ft).sort(function(a,b){return a-b;}).forEach(function(k){if(ft[k]){var d=document.createElement('div');d.className='feature';d.innerHTML='
'+ft[k]+'';g.appendChild(d);}});}})();
diff --git a/tests/blur-support-growth.test.mjs b/tests/blur-support-growth.test.mjs
new file mode 100644
index 00000000..d0472411
--- /dev/null
+++ b/tests/blur-support-growth.test.mjs
@@ -0,0 +1,46 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import {readFileSync} from 'node:fs';
+import {runInNewContext} from 'node:vm';
+const read = p => readFileSync(new URL('../'+p, import.meta.url), 'utf8');
+const html = read('support/blur/index.html');
+const langs = ['en','ja','zh','zh_TW','ko','es','pt','fr','de','it','ru','ar','th','id','tr','nl','vi'];
+
+test('first-rendered Blur benefits are verified single-player reveal mechanics',()=>{
+ for(const phrase of [
+ 'Daily blurred-image guessing challenge',
+ 'Reveal more of the picture step by step',
+ 'Guess before the full image appears',
+ 'Keep playing in infinite mode',
+ ]) assert.ok(html.includes(phrase),phrase);
+ assert.doesNotMatch(html,/Multiplayer mode|Timed challenges|Multiple difficulty levels/);
+});
+test('all 17 locale feature blocks communicate the same four real game actions',()=>{
+ const match=html.match(/const FEAT_T=(\{[\s\S]*?\});\s*const __oF=applyLang;/);
+ assert.ok(match,'missing localized feature block');
+ const dict=runInNewContext('('+match[1]+')');
+ assert.deepEqual(Object.keys(dict).sort(),[...langs].sort());
+ for(const locale of langs){
+ assert.deepEqual(Object.keys(dict[locale]).sort(),['0','1','2','3'],locale+' must show four verified actions');
+ for(const item of Object.values(dict[locale])) assert.ok(Array.from(item).length>=6,locale+' blank feature');
+ }
+ assert.match(dict.en[0],/daily/);
+ assert.match(dict.ja[3],/無限モード/);
+});
+test('first screen and detail Play links use the legitimate package with different placement',()=>{
+ const links=[...html.matchAll(/
l[2]).sort(),['details','hero']);
+ for(const link of links){
+ assert.ok(link[1].includes('id=com.blurapp.blur_app'));
+ assert.match(link[1],/referrer=utm_source%3Dappdevelopsk/);
+ assert.doesNotMatch(link[1],/user_id|device_id|email|quiz_answer/);
+ }
+ assert.match(html,/placement: a\.getAttribute\('data-position'\)/);
+});
+test('manual site deployment and read-only smoke include Blur landing',()=>{
+ assert.match(read('scripts/publish_app_discovery.sh'),/support\/blur\/index\.html/);
+ const live=read('scripts/check_app_discovery_live.sh');
+ assert.match(live,/fetch '\/support\/blur\/' 'blur.html'/);
+ assert.match(live,/FAIL Blur first-screen Play CTA not live/);
+ assert.match(live,/FAIL Blur advertises unverified game modes/);
+});