Polish dubbing workflow actions
This commit is contained in:
@@ -43,7 +43,7 @@ export default function DubHeader({
|
||||
tight 2px gap — title-first, owner-requested order. */}
|
||||
<div className="flex flex-wrap justify-between items-center gap-x-[var(--space-2)] gap-y-[4px] min-w-0">
|
||||
<div className="label-row dub-head__title !gap-[6px]">
|
||||
<FileText className="label-icon" size={11} />
|
||||
<FileText className="label-icon" size={11} aria-hidden="true" />
|
||||
<span className="font-medium text-[0.78rem] min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-fg normal-case">
|
||||
{dubFilename}
|
||||
</span>
|
||||
@@ -56,7 +56,7 @@ export default function DubHeader({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-[6px] items-center shrink-0">
|
||||
<div className="flex flex-wrap gap-[6px] items-center justify-end shrink-0 [.shell-mini_&]:w-full">
|
||||
{/* Icon-only secondary actions (tooltips carry the labels);
|
||||
Generate Dub keeps its label as the primary verb. */}
|
||||
<Button
|
||||
@@ -66,7 +66,7 @@ export default function DubHeader({
|
||||
title={t('dub.save')}
|
||||
aria-label={t('dub.save')}
|
||||
>
|
||||
<Save size={12} />
|
||||
<Save size={12} aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
@@ -75,24 +75,30 @@ export default function DubHeader({
|
||||
title={t('dub.reset')}
|
||||
aria-label={t('dub.reset')}
|
||||
>
|
||||
<RotateCcw size={12} />
|
||||
<RotateCcw size={12} aria-hidden="true" />
|
||||
</Button>
|
||||
{/* Primary actions live on the header bar (compact) — moved up from the footer. */}
|
||||
<div className="flex gap-[6px] items-center pl-[var(--space-2)] ml-[2px]">
|
||||
<div
|
||||
className="flex flex-wrap items-center justify-end gap-[4px] rounded-[var(--chrome-radius-pill)] bg-[color-mix(in_srgb,var(--color-bg-elev-2)_78%,transparent)] p-[3px] shadow-[var(--shadow-md)] [.shell-mini_&]:w-full"
|
||||
data-testid="dub-primary-actions"
|
||||
>
|
||||
{dubStep === 'stopping' ? (
|
||||
<FooterBtn
|
||||
sm
|
||||
tone="stopping"
|
||||
disabled
|
||||
icon={<Loader className="spinner" size={9} />}
|
||||
className="!flex-none [.shell-mini_&]:flex-1"
|
||||
icon={<Loader className="spinner" size={9} aria-hidden="true" />}
|
||||
label={t('dub.stopping')}
|
||||
aria-busy="true"
|
||||
/>
|
||||
) : dubStep === 'generating' ? (
|
||||
<FooterBtn
|
||||
sm
|
||||
tone="danger"
|
||||
className="!flex-none hover:-translate-y-px active:translate-y-0 motion-reduce:transform-none [.shell-mini_&]:flex-1"
|
||||
onClick={handleDubStop}
|
||||
icon={<Square size={9} />}
|
||||
icon={<Square size={9} aria-hidden="true" />}
|
||||
label={t('dub.stop_progress', {
|
||||
current: dubProgress.current,
|
||||
total: dubProgress.total,
|
||||
@@ -103,12 +109,13 @@ export default function DubHeader({
|
||||
<FooterBtn
|
||||
sm
|
||||
tone={dubSegments.length && !isTranslating ? 'pink' : 'idle'}
|
||||
className="dub-action-btn--generate !h-[30px] !flex-none !px-[11px] !text-[0.68rem] !font-semibold !tracking-[0.015em] enabled:hover:-translate-y-px enabled:active:translate-y-0 motion-reduce:transform-none enabled:shadow-[0_5px_14px_color-mix(in_srgb,var(--chrome-accent)_18%,transparent)] enabled:hover:shadow-[0_7px_18px_color-mix(in_srgb,var(--chrome-accent)_26%,transparent)] [.shell-mini_&]:flex-1"
|
||||
onClick={onGenerateClick}
|
||||
// The multi-language batch translates between generates while
|
||||
// dubStep briefly sits back at 'editing' — keep the CTA inert
|
||||
// during that phase so a re-click can't start a second batch.
|
||||
disabled={!dubSegments.length || isTranslating}
|
||||
icon={<Play size={11} />}
|
||||
icon={<Play className="fill-current" size={11} aria-hidden="true" />}
|
||||
label={
|
||||
multiLangMode && multiLangs.length > 1
|
||||
? t('dub.generate_dub_multi', {
|
||||
@@ -122,10 +129,11 @@ export default function DubHeader({
|
||||
<FooterBtn
|
||||
sm
|
||||
tone="pink"
|
||||
className="!h-[30px] !flex-none !px-[10px] enabled:hover:-translate-y-px enabled:active:translate-y-0 motion-reduce:transform-none [.shell-mini_&]:flex-1"
|
||||
onClick={() =>
|
||||
handleDubGenerate({ regenOnly: incrementalPlan.stale, preview: true })
|
||||
}
|
||||
icon={<Play size={11} />}
|
||||
icon={<Play className="fill-current" size={11} aria-hidden="true" />}
|
||||
label={t('dub.regen_changed', { count: incrementalPlan.stale.length })}
|
||||
/>
|
||||
)}
|
||||
@@ -135,23 +143,30 @@ export default function DubHeader({
|
||||
<FooterBtn
|
||||
sm
|
||||
tone="idle"
|
||||
className="dub-action-btn--verify !h-[30px] !flex-none !px-[9px] enabled:hover:-translate-y-px enabled:active:translate-y-0 motion-reduce:transform-none enabled:hover:shadow-[var(--shadow-sm)] [.shell-mini_&]:flex-1"
|
||||
disabled={qcRunning || !dubSegments.length}
|
||||
onClick={handleDubQc}
|
||||
icon={
|
||||
qcRunning ? <Loader className="spinner" size={11} /> : <ShieldCheck size={11} />
|
||||
qcRunning ? (
|
||||
<Loader className="spinner" size={11} aria-hidden="true" />
|
||||
) : (
|
||||
<ShieldCheck size={11} aria-hidden="true" />
|
||||
)
|
||||
}
|
||||
title={t('dub.qc_btn', { defaultValue: 'Verify dub timing (second-pass check)' })}
|
||||
aria-label={t('dub.qc_btn', {
|
||||
defaultValue: 'Verify dub timing (second-pass check)',
|
||||
})}
|
||||
label={t('dub.verify')}
|
||||
title={t('dub.qc_btn')}
|
||||
aria-label={t('dub.qc_btn')}
|
||||
aria-busy={qcRunning || undefined}
|
||||
/>
|
||||
)}
|
||||
<FooterBtn
|
||||
sm
|
||||
tone={dubStep === 'done' ? 'green' : 'idle'}
|
||||
className="dub-action-btn--export !h-[30px] !flex-none !px-[10px] !font-semibold enabled:hover:-translate-y-px enabled:active:translate-y-0 motion-reduce:transform-none enabled:shadow-[0_5px_14px_color-mix(in_srgb,var(--chrome-severity-ok)_13%,transparent)] enabled:hover:shadow-[0_7px_18px_color-mix(in_srgb,var(--chrome-severity-ok)_20%,transparent)] [.shell-mini_&]:flex-1"
|
||||
disabled={dubStep !== 'done' && !dubSegments.length}
|
||||
onClick={() => setExportOpen(true)}
|
||||
icon={<Download size={12} />}
|
||||
icon={<Download size={12} aria-hidden="true" />}
|
||||
label={t('dub.export_btn')}
|
||||
title={t('dub.export_btn')}
|
||||
aria-label={t('dub.export_btn')}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,9 @@ import React from 'react';
|
||||
const BASE =
|
||||
'inline-flex items-center justify-center gap-[5px] flex-1 mt-0 ' +
|
||||
'font-[family-name:var(--font-sans)] tracking-[0.02em] normal-case ' +
|
||||
'bg-transparent border rounded-[var(--chrome-radius-pill)] shadow-none transition-colors ' +
|
||||
'bg-transparent border rounded-[var(--chrome-radius-pill)] shadow-none touch-manipulation ' +
|
||||
'transition-[transform,background,color,box-shadow] duration-150 motion-reduce:transition-none ' +
|
||||
'focus-visible:outline-none focus-visible:shadow-[var(--focus-ring)] ' +
|
||||
'disabled:opacity-45 disabled:cursor-not-allowed';
|
||||
|
||||
const TONES = {
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "الدبلجة إلى",
|
||||
"transcript_after_extract": "يظهر النص بعد الاستخراج.",
|
||||
"qc_btn": "التحقق من توقيت الدبلجة (فحص المرور الثاني)",
|
||||
"verify": "تحقق",
|
||||
"qc_running": "جارٍ التحقق من توقيت الدبلجة…",
|
||||
"qc_result": "{{flagged}} من {{total}} قد تحتاج الأسطر إلى إعادة الاستماع",
|
||||
"qc_clean": "جميع أسطر {{total}} تتطابق مع النص",
|
||||
|
||||
@@ -905,6 +905,7 @@
|
||||
"target_language": "Dubben in",
|
||||
"transcript_after_extract": "Das Transkript erscheint nach der Extraktion.",
|
||||
"qc_btn": "Überprüfen des Dub-Timings (Second-Pass-Check)",
|
||||
"verify": "Prüfen",
|
||||
"qc_running": "Dub-Timing wird überprüft…",
|
||||
"qc_result": "{{flagged}} von {{total}} Zeilen müssen möglicherweise erneut abgehört werden",
|
||||
"qc_clean": "Alle {{total}}-Zeilen stimmen mit dem Skript überein",
|
||||
|
||||
@@ -1169,6 +1169,7 @@
|
||||
"stop_progress": "Stop ({{current}}/{{total}})",
|
||||
"regen_changed": "Regen {{count}} changed",
|
||||
"qc_btn": "Verify dub timing (second-pass check)",
|
||||
"verify": "Verify",
|
||||
"qc_running": "Checking dub timing…",
|
||||
"qc_result": "{{flagged}} of {{total}} lines may need a re-listen",
|
||||
"qc_clean": "All {{total}} lines match the script",
|
||||
|
||||
@@ -905,6 +905,7 @@
|
||||
"target_language": "Doblar a",
|
||||
"transcript_after_extract": "La transcripción aparecerá tras la extracción.",
|
||||
"qc_btn": "Verificar el tiempo de doblaje (verificación de segundo paso)",
|
||||
"verify": "Verificar",
|
||||
"qc_running": "Comprobando el tiempo de doblaje...",
|
||||
"qc_result": "Es posible que sea necesario volver a escuchar {{flagged}} de {{total}} líneas",
|
||||
"qc_clean": "Todas las líneas {{total}} coinciden con el guión.",
|
||||
|
||||
@@ -905,6 +905,7 @@
|
||||
"target_language": "Doubler en",
|
||||
"transcript_after_extract": "La transcription apparaît après l'extraction.",
|
||||
"qc_btn": "Vérifier le timing du doublage (vérification du deuxième passage)",
|
||||
"verify": "Vérifier",
|
||||
"qc_running": "Vérification du timing de doublage…",
|
||||
"qc_result": "{{flagged}} lignes sur {{total}} peuvent nécessiter une réécoute",
|
||||
"qc_clean": "Toutes les lignes {{total}} correspondent au script",
|
||||
|
||||
@@ -905,6 +905,7 @@
|
||||
"target_language": "इसमें डब करें",
|
||||
"transcript_after_extract": "निकालने के बाद प्रतिलिपि दिखाई देगी।",
|
||||
"qc_btn": "डब टाइमिंग सत्यापित करें (सेकंड-पास चेक)",
|
||||
"verify": "सत्यापित करें",
|
||||
"qc_running": "डब समय की जाँच की जा रही है...",
|
||||
"qc_result": "{{total}} पंक्तियों में से {{flagged}} को दोबारा सुनने की आवश्यकता हो सकती है",
|
||||
"qc_clean": "सभी {{total}} पंक्तियाँ स्क्रिप्ट से मेल खाती हैं",
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "Sulih suara ke",
|
||||
"transcript_after_extract": "Transkrip muncul setelah ekstraksi.",
|
||||
"qc_btn": "Verifikasi waktu sulih suara (pemeriksaan jalur kedua)",
|
||||
"verify": "Verifikasi",
|
||||
"qc_running": "Memeriksa waktu sulih suara…",
|
||||
"qc_result": "{{flagged}} dari {{total}} baris mungkin perlu didengarkan ulang",
|
||||
"qc_clean": "Semua baris {{total}} cocok dengan skrip",
|
||||
|
||||
@@ -905,6 +905,7 @@
|
||||
"target_language": "Doppia in",
|
||||
"transcript_after_extract": "La trascrizione appare dopo l'estrazione.",
|
||||
"qc_btn": "Verifica i tempi di duplicazione (controllo del secondo passaggio)",
|
||||
"verify": "Verifica",
|
||||
"qc_running": "Controllo dei tempi di duplicazione…",
|
||||
"qc_result": "{{flagged}} delle righe {{total}} potrebbe richiedere un nuovo ascolto",
|
||||
"qc_clean": "Tutte le righe {{total}} corrispondono allo script",
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "吹き替え先",
|
||||
"transcript_after_extract": "文字起こしは抽出後に表示されます。",
|
||||
"qc_btn": "ダビングタイミングの検証(2パス目チェック)",
|
||||
"verify": "検証",
|
||||
"qc_running": "ダビングのタイミングを確認中…",
|
||||
"qc_result": "{{total}} 行中 {{flagged}} 行は再試行が必要な可能性があります",
|
||||
"qc_clean": "すべての {{total}} 行がスクリプトと一致します",
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "더빙할 언어",
|
||||
"transcript_after_extract": "추출이 끝나면 대본이 표시됩니다.",
|
||||
"qc_btn": "더빙 타이밍 확인(두 번째 통과 확인)",
|
||||
"verify": "확인",
|
||||
"qc_running": "더빙 타이밍 확인 중…",
|
||||
"qc_result": "{{total}} 줄 중 {{flagged}} 줄을 다시 들어야 할 수 있습니다.",
|
||||
"qc_clean": "모든 {{total}} 줄은 스크립트와 일치합니다.",
|
||||
|
||||
@@ -905,6 +905,7 @@
|
||||
"target_language": "Dubben naar",
|
||||
"transcript_after_extract": "Het afschrift verschijnt na extractie.",
|
||||
"qc_btn": "Controleer de dub-timing (tweede-doorgangscontrole)",
|
||||
"verify": "Controleren",
|
||||
"qc_running": "De dubtiming controleren...",
|
||||
"qc_result": "{{flagged}} van {{total}} regels moeten mogelijk opnieuw worden beluisterd",
|
||||
"qc_clean": "Alle {{total}} regels komen overeen met het script",
|
||||
|
||||
@@ -905,6 +905,7 @@
|
||||
"target_language": "Dubbinguj na",
|
||||
"transcript_after_extract": "Transkrypcja pojawi się po ekstrakcji.",
|
||||
"qc_btn": "Sprawdź czas dubowania (kontrola drugiego przejścia)",
|
||||
"verify": "Sprawdź",
|
||||
"qc_running": "Sprawdzam synchronizację dubbingu…",
|
||||
"qc_result": "{{flagged}} z {{total}} wierszy może wymagać ponownego przesłuchania",
|
||||
"qc_clean": "Wszystkie linie {{total}} odpowiadają skryptowi",
|
||||
|
||||
@@ -905,6 +905,7 @@
|
||||
"target_language": "Dublar para",
|
||||
"transcript_after_extract": "A transcrição aparece após a extração.",
|
||||
"qc_btn": "Verifique o tempo de dublagem (verificação de segunda passagem)",
|
||||
"verify": "Verificar",
|
||||
"qc_running": "Verificando o tempo de dublagem…",
|
||||
"qc_result": "{{flagged}} de {{total}} linhas podem precisar de uma nova escuta",
|
||||
"qc_clean": "Todas as linhas {{total}} correspondem ao script",
|
||||
|
||||
@@ -905,6 +905,7 @@
|
||||
"target_language": "Дублировать на",
|
||||
"transcript_after_extract": "Стенограмма появится после извлечения.",
|
||||
"qc_btn": "Проверка синхронизации перезаписи (вторая проверка)",
|
||||
"verify": "Проверить",
|
||||
"qc_running": "Проверка синхронизации дубляжа…",
|
||||
"qc_result": "{{flagged}} из {{total}} строк может потребоваться повторное прослушивание",
|
||||
"qc_clean": "Все строки {{total}} соответствуют сценарию",
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "Dubba till",
|
||||
"transcript_after_extract": "Avskriften visas efter extraktionen.",
|
||||
"qc_btn": "Verifiera dubbningstid (second-pass check)",
|
||||
"verify": "Verifiera",
|
||||
"qc_running": "Kontrollerar dubbningstid...",
|
||||
"qc_result": "{{flagged}} av {{total}} rader kan behöva lyssnas om",
|
||||
"qc_clean": "Alla {{total}} rader matchar skriptet",
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "พากย์เป็น",
|
||||
"transcript_after_extract": "ทรานสคริปต์จะปรากฏหลังการแยกเสียง",
|
||||
"qc_btn": "ตรวจสอบระยะเวลาการพากย์ (ตรวจสอบรอบที่สอง)",
|
||||
"verify": "ตรวจสอบ",
|
||||
"qc_running": "กำลังตรวจสอบเวลาพากย์...",
|
||||
"qc_result": "{{flagged}} จาก {{total}} บรรทัดอาจต้องฟังซ้ำ",
|
||||
"qc_clean": "{{total}} บรรทัดทั้งหมดตรงกับสคริปต์",
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "Dublaj dili",
|
||||
"transcript_after_extract": "Transkript, çıkarma işleminden sonra görünür.",
|
||||
"qc_btn": "Dub zamanlamasını doğrulayın (ikinci geçiş kontrolü)",
|
||||
"verify": "Doğrula",
|
||||
"qc_running": "Dub zamanlaması kontrol ediliyor…",
|
||||
"qc_result": "{{flagged}} / {{total}} satırların yeniden dinlenmesi gerekebilir",
|
||||
"qc_clean": "Tüm {{total}} satırları komut dosyasıyla eşleşiyor",
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "Дублювати мовою",
|
||||
"transcript_after_extract": "Стенограма з’явиться після вилучення.",
|
||||
"qc_btn": "Перевірте синхронізацію дубляжу (перевірка другого проходу)",
|
||||
"verify": "Перевірити",
|
||||
"qc_running": "Перевірка часу дубляжу…",
|
||||
"qc_result": "{{flagged}} з {{total}} рядків може знадобитися переслухати",
|
||||
"qc_clean": "Усі {{total}} рядки відповідають сценарію",
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "Lồng tiếng sang",
|
||||
"transcript_after_extract": "Bản chép lời sẽ xuất hiện sau khi trích xuất.",
|
||||
"qc_btn": "Xác minh thời gian lồng tiếng (kiểm tra lần thứ hai)",
|
||||
"verify": "Xác minh",
|
||||
"qc_running": "Đang kiểm tra thời gian lồng tiếng…",
|
||||
"qc_result": "{{flagged}} trong số {{total}} dòng có thể cần nghe lại",
|
||||
"qc_clean": "Tất cả các dòng {{total}} đều khớp với tập lệnh",
|
||||
|
||||
@@ -866,6 +866,7 @@
|
||||
"target_language": "配音为",
|
||||
"transcript_after_extract": "提取完成后将显示转录文本。",
|
||||
"qc_btn": "验证配音时序(第二遍检查)",
|
||||
"verify": "验证",
|
||||
"qc_running": "检查配音时间...",
|
||||
"qc_result": "{{flagged}} 行(共 {{total}} 行)可能需要重新收听",
|
||||
"qc_clean": "所有 {{total}} 行都与脚本匹配",
|
||||
|
||||
@@ -907,6 +907,7 @@
|
||||
"target_language": "配音成",
|
||||
"transcript_after_extract": "擷取完成後將顯示逐字稿。",
|
||||
"qc_btn": "驗證配音時序(第二遍檢視)",
|
||||
"verify": "驗證",
|
||||
"qc_running": "檢查配音時間...",
|
||||
"qc_result": "{{flagged}} 行(共 {{total}} 行)可能需要重新聆聽",
|
||||
"qc_clean": "所有 {{total}} 行都與腳本匹配",
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import i18n from '../i18n';
|
||||
|
||||
import DubHeader from '../components/dub/DubHeader';
|
||||
|
||||
const t = i18n.t.bind(i18n);
|
||||
|
||||
function makeProps(overrides = {}) {
|
||||
return {
|
||||
t,
|
||||
dubFilename: 'sample.mp4',
|
||||
dubDuration: 42,
|
||||
dubSegments: [{ id: 'segment-1' }],
|
||||
activeProjectName: '',
|
||||
saveProject: vi.fn(),
|
||||
resetDub: vi.fn(),
|
||||
dubStep: 'done',
|
||||
handleDubStop: vi.fn(),
|
||||
dubProgress: { current: 0, total: 0 },
|
||||
onGenerateClick: vi.fn(),
|
||||
isTranslating: false,
|
||||
multiLangMode: false,
|
||||
multiLangs: [],
|
||||
incrementalPlan: { stale: [], fresh: [] },
|
||||
handleDubGenerate: vi.fn(),
|
||||
qcRunning: false,
|
||||
handleDubQc: vi.fn(),
|
||||
setExportOpen: vi.fn(),
|
||||
pipelineSteps: [],
|
||||
onPipelineStep: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('DubHeader — polished workflow actions', () => {
|
||||
it('keeps all three actions visible, ordered, accessible, and wired', () => {
|
||||
const props = makeProps();
|
||||
render(<DubHeader {...props} />);
|
||||
|
||||
const group = screen.getByTestId('dub-primary-actions');
|
||||
const buttons = within(group).getAllByRole('button');
|
||||
const generate = within(group).getByRole('button', { name: t('dub.generate_dub') });
|
||||
const verify = within(group).getByRole('button', { name: t('dub.qc_btn') });
|
||||
const exportButton = within(group).getByRole('button', { name: t('dub.export_btn') });
|
||||
|
||||
expect(buttons).toEqual([generate, verify, exportButton]);
|
||||
expect(generate).toHaveTextContent(t('dub.generate_dub'));
|
||||
expect(verify).toHaveTextContent(t('dub.verify'));
|
||||
expect(exportButton).toHaveTextContent(t('dub.export_btn'));
|
||||
expect(generate).toHaveClass('dub-action-btn--generate');
|
||||
expect(verify).toHaveClass('dub-action-btn--verify');
|
||||
expect(exportButton).toHaveClass('dub-action-btn--export');
|
||||
expect(group.querySelectorAll('svg[aria-hidden="true"]')).toHaveLength(3);
|
||||
|
||||
fireEvent.click(generate);
|
||||
fireEvent.click(verify);
|
||||
fireEvent.click(exportButton);
|
||||
expect(props.onGenerateClick).toHaveBeenCalledOnce();
|
||||
expect(props.handleDubQc).toHaveBeenCalledOnce();
|
||||
expect(props.setExportOpen).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('wraps and stretches actions in mini shells without changing their labels', () => {
|
||||
render(<DubHeader {...makeProps()} />);
|
||||
|
||||
const group = screen.getByTestId('dub-primary-actions');
|
||||
expect(group).toHaveClass('flex-wrap', '[.shell-mini_&]:w-full');
|
||||
for (const button of within(group).getAllByRole('button')) {
|
||||
expect(button).toHaveClass('[.shell-mini_&]:flex-1', 'motion-reduce:transform-none');
|
||||
}
|
||||
});
|
||||
|
||||
it('exposes verification progress and preserves disabled action guards', () => {
|
||||
const { rerender } = render(<DubHeader {...makeProps({ qcRunning: true })} />);
|
||||
const verify = screen.getByRole('button', { name: t('dub.qc_btn') });
|
||||
expect(verify).toBeDisabled();
|
||||
expect(verify).toHaveAttribute('aria-busy', 'true');
|
||||
|
||||
rerender(
|
||||
<DubHeader {...makeProps({ dubStep: 'editing', dubSegments: [], incrementalPlan: null })} />,
|
||||
);
|
||||
expect(screen.getByRole('button', { name: t('dub.generate_dub') })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: t('dub.export_btn') })).toBeDisabled();
|
||||
expect(screen.queryByRole('button', { name: t('dub.qc_btn') })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user