Skip to content

Commit 80e5cae

Browse files
authored
Follow-ups: a missing translation key, the last gift emoji, and a test that did not test (#89)
Three loose ends from #87/#88, found reading back over what was merged. **common.clear existed in neither locale.** ImageField uses it for the clear button's tooltip, so that button's title read the literal string "common.clear". TypeScript could not catch it: tr is typed as `typeof en` and the key was missing from both, so the shapes still matched. **#80 said the SVG badge replaces the gift emoji "in the storefront and in both admin product lists", and the web panel's Manage list still had the emoji.** The storefront and the desktop list were done; that one was not. It uses the same CRATE_ICON_SVG now. **The assertion written to catch the first of those did not catch it.** It scanned document.body.innerText for anything shaped like a translation key, which sounds right and misses every key used in a title, placeholder or aria-label - `common.clear` among them, since innerText contains no attributes. Worse, the clear button only renders once an image field has a value, so an untouched editor never draws it at all. Fixed by scanning attributes too and by filling the icon field first, through the native value setter so React notices. Then proved: with the key removed the gate now exits 1 with "untranslated keys rendered in the store view: common.clear", and with it restored it passes. A test that cannot fail is worse than no test, because it reports coverage that is not there - the same shape as the single-instance-lock false pass fixed in the previous PR. The tab sweep already mounts every view, so nothing was crashing; what was missing was any assertion about what the Store view actually renders. It now also checks the crate animation picker and image fields are present, so #75 and #76 have renderer-level coverage rather than only shared-logic coverage. All twelve smoke gates pass.
1 parent e0a012e commit 80e5cae

4 files changed

Lines changed: 86 additions & 2 deletions

File tree

‎src/main/smoke.ts‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,6 +2284,87 @@ export async function runSmoke(): Promise<void> {
22842284
)
22852285
await sleep(300)
22862286
if (await viewCrashed()) return fail('create view crashed')
2287+
// ---- the Store view, mounted for real ----
2288+
//
2289+
// The tab sweep above proves nothing crashes, which is not the same as
2290+
// nothing being broken. A `t()` call for a key that exists in neither locale
2291+
// renders the key itself - `common.clear` did exactly that - and TypeScript
2292+
// cannot catch it, because `tr` is typed as `typeof en` and both were simply
2293+
// missing it. So: open the product editor and assert no raw key is on screen.
2294+
{
2295+
await win.webContents.executeJavaScript(`document.querySelector('.server-item')?.click()`)
2296+
await sleep(250)
2297+
await win.webContents.executeJavaScript(
2298+
`[...document.querySelectorAll('.tab')].find(b=>/Store|Mağaza/i.test(b.textContent||''))?.click()`
2299+
)
2300+
await sleep(300)
2301+
// The catalogue lives behind the second section tab.
2302+
await win.webContents.executeJavaScript(
2303+
`[...document.querySelectorAll('.tabs .tab')].pop()?.click()`
2304+
)
2305+
await sleep(250)
2306+
// "Add crate" opens the editor with every new control on it.
2307+
await win.webContents.executeJavaScript(
2308+
`[...document.querySelectorAll('button')].find(b=>/Add crate|Sandık ekle|Kasa ekle/i.test(b.textContent||''))?.click()`
2309+
)
2310+
await sleep(400)
2311+
if (await viewCrashed()) return fail('the store product editor crashed on mount')
2312+
2313+
// Put a value in the icon field first. Half the controls on an image field
2314+
// only exist once it has one - the clear button among them - so probing an
2315+
// untouched editor would miss exactly the strings least likely to be
2316+
// translated. React owns the input, so the native setter plus a bubbling
2317+
// event is what makes it notice.
2318+
await win.webContents.executeJavaScript(`(()=>{
2319+
const inp = document.querySelector('input[placeholder*="uploads"]');
2320+
if (!inp) return;
2321+
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
2322+
setter.call(inp, 'https://example.invalid/icon.png');
2323+
inp.dispatchEvent(new Event('input', { bubbles: true }));
2324+
})()`)
2325+
await sleep(250)
2326+
2327+
const storeProbe = await win.webContents.executeJavaScript(`(()=>{
2328+
// Attributes as well as text. A missing key is just as broken in a
2329+
// tooltip, and innerText does not contain one - the first version of this
2330+
// assertion read only innerText and passed with common.clear missing,
2331+
// which is the exact bug it was written for.
2332+
let hay = document.body.innerText || '';
2333+
for (const el of document.querySelectorAll('[title],[placeholder],[aria-label]')) {
2334+
hay += ' ' + (el.getAttribute('title') || '') +
2335+
' ' + (el.getAttribute('placeholder') || '') +
2336+
' ' + (el.getAttribute('aria-label') || '');
2337+
}
2338+
// A translation key that resolved to nothing looks exactly like its key.
2339+
const raw = (hay.match(/\\b(?:store|common|web)\\.[a-zA-Z_][a-zA-Z0-9_.-]*/g) || []);
2340+
return JSON.stringify({
2341+
raw: [...new Set(raw)],
2342+
modal: !!document.querySelector('.modal'),
2343+
anim: [...document.querySelectorAll('select')].some(s =>
2344+
[...s.options].some(o => /reel/i.test(o.value))),
2345+
imageFields: document.querySelectorAll('input[placeholder*="uploads"]').length
2346+
})
2347+
})()`)
2348+
const sp = JSON.parse(storeProbe) as {
2349+
raw: string[]
2350+
modal: boolean
2351+
anim: boolean
2352+
imageFields: number
2353+
}
2354+
if (!sp.modal) return fail('the store product editor did not open; probe=' + storeProbe)
2355+
if (sp.raw.length) return fail('untranslated keys rendered in the store view: ' + sp.raw.join(', '))
2356+
if (!sp.anim) return fail('the crate editor has no animation picker (#75)')
2357+
if (sp.imageFields < 1) return fail('the crate editor has no image field (#76)')
2358+
// Close it again so the file-editor assertions below start from a clean view.
2359+
await win.webContents.executeJavaScript(
2360+
`[...document.querySelectorAll('.modal-actions button')][0]?.click()`
2361+
)
2362+
await sleep(200)
2363+
console.log(
2364+
'SMOKE: store editor OK (opens, animation picker + image fields present, no untranslated keys)'
2365+
)
2366+
}
2367+
22872368
// Return to a server view (settings/create have no tab bar), then open a file
22882369
// to verify the CodeMirror editor mounts.
22892370
await win.webContents.executeJavaScript(`document.querySelector('.server-item')?.click()`)

‎src/main/web/panelHtml.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ ${STORE_CSS}
133133
/* store admin */
134134
.mrow{display:flex;gap:10px;align-items:center;padding:9px 0;border-bottom:1px solid var(--border)}
135135
.mrow:last-child{border-bottom:none}
136-
.mrow .ic{width:20px;text-align:center;flex:none}
136+
.mrow .ic{width:20px;text-align:center;flex:none;display:inline-flex;align-items:center;justify-content:center}
137+
.mrow .ic svg{width:15px;height:15px;color:var(--accent)}
137138
.pm-modal{position:fixed;inset:0;background:rgba(0,0,0,.72);display:grid;place-items:center;z-index:50;padding:16px}
138139
.pm-box{background:linear-gradient(160deg,#17151b,#0c0c11);border:1px solid var(--border);border-radius:16px;padding:20px;width:min(560px,95vw);max-height:88vh;overflow:auto;box-shadow:0 30px 70px rgba(0,0,0,.65)}
139140
.pm-box label{display:block;font-size:12px;color:var(--dim);margin-top:8px}
@@ -731,7 +732,7 @@ function saveCurrency(){var c=document.getElementById('mCur').value.trim()||'Coi
731732
api('/api/servers/'+current.id+'/store/admin/currency',{method:'POST',body:JSON.stringify({currency:c})}).then(function(r){if(!r.ok){alert('Could not save currency');return}loadManage()})}
732733
function renderMProducts(){var el=document.getElementById('mProducts');var ps=mstore.products||[];
733734
if(!ps.length){el.innerHTML='<div class="dim">No products yet.</div>';return}
734-
el.innerHTML=ps.map(function(p){return '<div class="mrow"'+(p.hidden?' style="opacity:.55"':'')+'><span class="ic">'+(p.type==='crate'?'🎁':'📦')+'</span>'+
735+
el.innerHTML=ps.map(function(p){return '<div class="mrow"'+(p.hidden?' style="opacity:.55"':'')+'><span class="ic">'+(p.type==='crate'?CRATE_ICON_SVG:'📦')+'</span>'+
735736
'<div style="flex:1;min-width:0"><div style="font-weight:700">'+esc(p.name)+
736737
(p.hidden?' <span class="badge">hidden</span>':'')+
737738
(typeof p.stock==='number'?' <span class="badge">'+(p.stock?p.stock+' left':'sold out')+'</span>':'')+'</div>'+

‎src/renderer/src/locales/en.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,7 @@ export default {
871871
yes: 'Yes',
872872
no: 'No',
873873
close: 'Close',
874+
clear: 'Clear',
874875
loading: 'Loading…',
875876
refresh: 'Refresh',
876877
apply: 'Apply',

‎src/renderer/src/locales/tr.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,7 @@ const tr: typeof en = {
876876
yes: 'Evet',
877877
no: 'Hayır',
878878
close: 'Kapat',
879+
clear: 'Temizle',
879880
loading: 'Yükleniyor…',
880881
refresh: 'Yenile',
881882
apply: 'Uygula',

0 commit comments

Comments
 (0)