8000 GitHub - iid7oom21/Abdulrhman-: Abdulrhman · GitHub
[go: up one dir, main page]

Skip to content

About

Abdulrhman

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

.# RAVEN — وحدة التحكم في مشغل الموسيقى (14 أغنية) ودمج صفحة المشروع

يتضمن هذا المستودع:

  1. التجربة المعزولة لوحدة التحكم في المشغل (player-controller.mjs و player-controller.test.mjs): إصلاح شامل لسباقات العمليات غير المتزامنة (audio.play() Promises) على قائمة من 14 أغنية، مع اختبارات حتمية عبر node:test.
  2. دمج صفحة مشروع RAVEN الكاملة (/raven/#top) ومعاينة الحوار (/dialogue): خادم موحّد (server.ts) يعمل على المنفذ 3000 ويقدّم صفحة RAVEN السينمائية الأصلية ومشغلها المدمج ومعاينة الحوار التفاعلية ومختبر فحص وحدة التحكم (/player-lab).

أولًا: العيوب التي أُثبتت في المصدر الأصلي (createLegacyPlayer)

في النموذج الأصلي المبسط:

async function play() {
  await audio.play();
  playing = true;
}

لا تتغير قيمة playing إلى true إلا بعد تسوية Promise الخاص بـ audio.play(). وقد أثبتنا عبر الاختبارين الأولين في player-controller.test.mjs عيبين رئيسيين بالنتائج الفعلية وتسلسل الأحداث:

1. الضغط على تشغيل ثم الضغط على toggle() قبل اكتمال Promise الأول

  • تسلسل الأحداث الفعلي في المصدر الأصلي:
    1. legacy.play() يستدعي audio.play()#1، لكن playing تبقى false طالما أن الوعد معلّق.
    2. المستخدم يضغط legacy.toggle() بنية الإيقاف، فيفحص المشغل if (playing) فيجدها false، فيستدعي play() للمرة الثانية (audio.play()#2) بدل استدعاء pause().
    3. عند تسوية الوعدين (resolve#1, resolve#2)، ينتهي المشغل في الحالة { index: 0, playing: true } مع playCalls = 2 و pauseCalls = 0، رغم أن آخر أمر للمستخدم كان الإيقاف.
  • سجل الأحداث المسجل في الاختبار: src:track-01 -> play()#1 -> play()#2 -> resolve#1 -> resolve#2

2. بدء تشغيل الأغنية الأولى ثم اختيار أغنية أخرى (select(1)) قبل اكتمال Promise

  • تسلسل الأحداث الفعلي في المصدر الأصلي:
    1. legacy.play() يبدأ طلب تشغيل الأغنية 0، وتظل playing === false أثناء انتظار التسوية.
    2. legacy.select(1) يقرأ const resume = playing فيحصل على false، فيستدعي pause() ويغيّر index = 1 و audio.src = tracks[1].src، لكنه يتجاهل تشغيل الأغنية الجديدة لأن resume === false.
    3. عندما ينجح وعد audio.play() القديم الخاص بالأغنية 0 متأخرًا، ينفذ السطر playing = true، فتصبح حالة المشغل { index: 1, playing: true } كذبًا، رغم أن الأغنية 1 لم يُستدعَ لها play() أبدًا (playCalls = 1).

ثانيًا: كيف يمنع الإصلاح تأثير النتائج القديمة (Stale Promises)

في createPlayer(audio, tracks) داخل player-controller.mjs:

  1. معرّف العملية المتزايد (operationId):

    • كل أمر يغيّر حالة التشغيل (play, pause, select, dispose) يزيد العداد operationId += 1.
    • عند استدعاء play()، يُحفظ المعرف المحلي const currentOp = operationId.
    • عندما يُسوى وعد audio.play() (سواء بالنجاح then أو بالرفض catch)، يتحقق المتحكم أولًا من:
      if (disposed || operationId !== currentOp) {
        return false;
      }
    • إذا كان المستخدم قد ضغط إيقافًا، أو بدّل الأغنية، أو أطلق طلب تشغيل أحدث، أو استدعى dispose()، فإن operationId يكون قد تغيّر، فتُهمل نتيجة الوعد القديم تمامًا دون المساس بالحالة الأحدث.
  2. منع استدعاءات play() المكررة أثناء الانتظار:

    • إذا كانت الحالة loading ويوجد طلب جارٍ (activePlayPromise)، فإن استدعاء play() المباشر يعيد نفس الوعد الجاهز دون إطلاق استدعاء audio.play() إضافي.
    • أما الضغط على toggle() أثناء loading فيقرأ desiredPlaying === true ويترجمه فورًا إلى أمر إيقاف (pause()).

ثالثًا: كيفية تمثيل حالات المشغل (State Model)

يفصل المشغل المصلح بين ثلاث طبقات مستقلة في getState():

الحقل النوع الدلالة
status 'paused' | 'loading' | 'playing' | 'error' | 'disposed' الحالة الصريحة للمشغل في اللحظة الحالية
desiredPlaying boolean نية المستخدم: true منذ لحظة طلب التشغيل (loading أو playing)، و false عند الإيقاف أو الخطأ أو التخلص (dispose)
pending boolean انتظار بدء التشغيل: true فقط أثناء انتظار تسوية audio.play() (status === 'loading')
playing boolean التشغيل المؤكد: true فقط بعد نجاح audio.play() للعملية الأحدث (status === 'playing')
index number فهرس الأغنية الحالية (0 إلى tracks.length - 1)
error Error | null كائن الخطأ عند رفض play() أو رميه استثناءً متزامنًا (دون إعادة محاولة تلقائية)
disposed boolean true بعد استدعاء dispose() وفك مستمع ended وإيقاف الصوت
operationId number رقم العملية الحالية لعزل التسويات المتأخرة
trackCount number عدد الأغاني المستخرج ديناميكيًا من tracks.length (14 في بيانات التجربة)
src string مسار الأغنية المختارة حاليًا

سلوك الحالات الحدية:

  • القائمة الفارغة أو غير الصالحة: يرمي createPlayer(audio, []) خطأ RangeError واضحًا، ويرمي TypeError إذا لم تكن القائمة مصفوفة أو خلا أي مدخل من src نصي غير فارغ.
  • الفهرس غير الصحيح في select(nextIndex): أي قيمة غير صحيحة (-1, 14, 1.5, NaN, '2', null, undefined) ترمي RangeError قبل تنفيذ أي تعديل، فتبقى الأغنية الحالية وحالة التشغيل دون أي تغيير أو انقطاع.

رابعًا: أمر الاختبار ونتائج التشغيل الفعلية

تم التنفيذ والتحقق فعليًا في بيئة Linux باستخدام:

  • إصدار Node.js: v22.23.3
  • أمر الاختبار:
    node --test player-controller.test.mjs

سجل التشغيل الفعلي:

TAP version 13
# Subtest: أولًا: إثبات العيوب في المصدر الأصلي (Legacy Bug Reproduction)
    # Subtest: عيب 1 (المصدر الأصلي): الضغط على تشغيل ثم toggle قبل اكتمال Promise الأول يستدعي play() مرتين وينتهي بحالة تشغيل بدل الإيقاف
    ok 1 - عيب 1 (المصدر الأصلي): الضغط على تشغيل ثم toggle قبل اكتمال Promise الأول يستدعي play() مرتين وينتهي بحالة تشغيل بدل الإيقاف
    # Subtest: عيب 2 (المصدر الأصلي): بدء تشغيل الأغنية الأولى ثم اختيار أغنية أخرى قبل اكتمال Promise يفقد نية التشغيل وتغيّر النتيجة القديمة الحالة
    ok 2 - عيب 2 (المصدر الأصلي): بدء تشغيل الأغنية الأولى ثم اختيار أغنية أخرى قبل اكتمال Promise يفقد نية التشغيل وتغيّر النتيجة القديمة الحالة
    1..2
ok 1 - أولًا: إثبات العيوب في المصدر الأصلي (Legacy Bug Reproduction)
# Subtest: ثانيًا وثالثًا: اختبارات الوحدة المصلحة (Fixed Player Controller)
    # Subtest: 1. الحالة الابتدائية دون تشغيل تلقائي
    ok 1 - 1. الحالة الابتدائية دون تشغيل تلقائي
    # Subtest: 2. تشغيل ناجح ثم إيقاف
    ok 2 - 2. تشغيل ناجح ثم إيقاف
    # Subtest: 3. تشغيل معلق ثم إيقاف عبر toggle قبل اكتماله (دون استدعاء play إضافي)
    ok 3 - 3. تشغيل معلق ثم إيقاف عبر toggle قبل اكتماله (دون استدعاء play إضافي)
    # Subtest: 4. نجاح قديم يصل بعد الإيقاف الصريح لا يعيد المشغل إلى playing
    ok 4 - 4. نجاح قديم يصل بعد الإيقاف الصريح لا يعيد المشغل إلى playing
    # Subtest: 5. رفض قديم يصل بعد بدء طلب أحدث لا يلوث حالة الطلب الأحدث
    ok 5 - 5. رفض قديم يصل بعد بدء طلب أحدث لا يلوث حالة الطلب الأحدث
    # Subtest: 6. تغيير الأغنية أثناء انتظار التشغيل ينقل قصد التشغيل للأغنية الجديدة ويتجاهل نجاح القديم
    ok 6 - 6. تغيير الأغنية أثناء انتظار التشغيل ينقل قصد التشغيل للأغنية الجديدة ويتجاهل نجاح القديم
    # Subtest: 7. تغيير الأغنية أثناء الإيقاف يبقي المشغل متوقفًا دون استدعاء play()
    ok 7 - 7. تغيير الأغنية أثناء الإيقاف يبقي المشغل متوقفًا دون استدعاء play()
    # Subtest: 8. عدة ضغطات تشغيل وإيقاف متتالية (Rapid Toggle Sequence)
    ok 8 - 8. عدة ضغطات تشغيل وإيقاف متتالية (Rapid Toggle Sequence)
    # Subtest: 9. تبديلات سريعة بين أغنيات متعددة أثناء انتظار التشغيل
    ok 9 - 9. تبديلات سريعة بين أغنيات متعددة أثناء انتظار التشغيل
    # Subtest: 10. رفض التشغيل يترك حالة خطأ واضحة دون إعادة محاولة تلقائية، ويسمح بإعادة محاولة صريحة ناجحة
    ok 10 - 10. رفض التشغيل يترك حالة خطأ واضحة دون إعادة محاولة تلقائية، ويسمح بإعادة محاولة صريحة ناجحة
    # Subtest: 11. خطأ متزامن (Synchronous Throw) من audio.play() يُلتقط بأمان ويسمح بالمحاولة مجددًا
    ok 11 - 11. خطأ متزامن (Synchronous Throw) من audio.play() يُلتقط بأمان ويسمح بالمحاولة مجددًا
    # Subtest: 12. الانتقال عبر الأغنيات الأربع عشرة عند انتهاء كل أغنية ثم العودة من 14 إلى الأولى
    ok 12 - 12. الانتقال عبر الأغنيات الأربع عشرة عند انتهاء كل أغنية ثم العودة من 14 إلى الأولى
    # Subtest: 13. حدث ended بعد طلب المستخدم الإيقاف (أو أثناء الانتظار) لا يعيد التشغيل ولا يغير الأغنية
    ok 13 - 13. حدث ended بعد طلب المستخدم الإيقاف (أو أثناء الانتظار) لا يعيد التشغيل ولا يغير الأغنية
    # Subtest: 14. dispose() أثناء انتظار التشغيل يوقف الصوت ويفك المستمعين ويمنع النتائج المتأخرة من تغيير الحالة
    ok 14 - 14. dispose() أثناء انتظار التشغيل يوقف الصوت ويفك المستمعين ويمنع النتائج المتأخرة من تغيير الحالة
    # Subtest: 15. القائمة الفارغة والفهرس غير الصحيح يرميان أخطاء واضحة دون إفساد حالة المشغل
    ok 15 - 15. القائمة الفارغة والفهرس غير الصحيح يرميان أخطاء واضحة دون إفساد حالة المشغل
    1..15
ok 2 - ثانيًا وثالثًا: اختبارات الوحدة المصلحة (Fixed Player Controller)
1..2
# tests 17
# suites 2
# pass 17
# fail 0

خامسًا: حدود الاختبارات والفصل بين منطق التحكم واختبار المتصفح

  • ما تثبته اختبارات FakeAudio (player-controller.test.mjs):
    • صحة آلة الحالة (State Machine) وعزل الوعود غير المتزامنة (Promises) وترتيب التسويات مهما تأخرت أو تداخلت.
    • عدم ضياع نية المستخدم عند التبديل السريع بين الأغاني الأربع عشرة أو الضغط المتتالي على toggle().
    • الالتفاف الصحيح من الأغنية 14 إلى الأغنية 1 عند حدث ended، والتنظيف الكامل عبر dispose().
  • ما لا تثبته اختبارات FakeAudio ويحتاج إلى متصفح وأجهزة فعلية:
    • وجود ملفات الصوت الحقيقية أو سلامة ترميزها عبر الشبكة (HTTP 200/206 Range Requests).
    • سياسات التشغيل التلقائي (Autoplay Policy / NotAllowedError) التي يفرضها المتصفح قبل تفاعل المستخدم (User Gesture).
    • سلوك HTMLMediaElement و AudioContext الفعلي على متصفح Safari في iOS/iPadOS (مثل قيود audio.volume وإطلاق أحداث waiting و stalled و AbortError عند تغيير audio.src).

About

Abdulrhman

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0