§
    ¢"¿j-;  ã                  ó  — d Z ddlmZ ddlZddlZddlmZ ddlZ ee¦  «         	                    ¦   «         j
        j
        j
        Zdd	„Z G d
„ d¦  «        Z G d„ d¦  «        Z G d„ d¦  «        Z G d„ d¦  «        Z G d„ d¦  «        ZdS )zÇ
Unit Test Suite for Client WebApp UI Modules (Milestone 5)
Empirical verification of PartyUI, SocialUI, TeleportHUD, TownPortalController,
and 9-Language i18n Catalog Parity. Soft Cap <= 350 lines.
é    )ÚannotationsN)ÚPathÚscript_bodyÚstrÚreturnÚdictc                ó  — d| › d�}t          j        ddd|gddt          t          ¦  «        ¬¦  «        }|j        dk    rt          d	|j        › d
|j        › �¦  «        ‚t          j	        |j         
                    ¦   «         ¦  «        S )z-Runs a simulated DOM test harness in Node.js.aç	  
import fs from 'fs';

function setupDOM() {
  const elMap = {}, winListeners = {}, docListeners = {};
  class HTMLElement {}
  class Image { constructor() { this.src = ''; } }
  class CustomEvent { constructor(t, i = {}) { this.type = t; this.detail = i.detail || {}; } }
  function makeEl(id, tag = 'DIV') {
    const classes = new Set(), handlers = {}, children = [];
    let _html = '';
    return {
      id, tagName: tag, children, dataset: {}, style: { width: '0%' }, value: '',
      classList: { add: (c) => classes.add(c), remove: (c) => classes.delete(c), contains: (c) => classes.has(c) },
      get innerHTML() { return _html; }, set innerHTML(val) { _html = val; children.length = 0; },
      addEventListener: (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); },
      removeEventListener: (t, fn) => { if (handlers[t]) handlers[t] = handlers[t].filter(f => f !== fn); },
      dispatchEvent: function(evt) { evt.target = evt.target || this; evt.currentTarget = this; (handlers[evt.type] || []).forEach(fn => fn(evt)); },
      querySelector: (sel) => makeEl(id + '_' + sel), querySelectorAll: (sel) => [makeEl(id + '_1'), makeEl(id + '_2')],
      appendChild: (c) => { children.push(c); return c; }
    };
  }
  const win = {
    addEventListener: (t, fn) => { (winListeners[t] = winListeners[t] || []).push(fn); },
    removeEventListener: (t, fn) => { if (winListeners[t]) winListeners[t] = winListeners[t].filter(f => f !== fn); },
    dispatchEvent: (evt) => { (winListeners[evt.type] || []).forEach(fn => fn(evt)); return true; },
    CustomEvent, HTMLElement, Image, FreeExileI18n: null
  };
  const doc = {
    getElementById: (id) => elMap[id] || (elMap[id] = makeEl(id)),
    querySelector: (sel) => elMap[sel.replace('#', '')] || makeEl(sel),
    querySelectorAll: () => [], createElement: (tag) => makeEl('auto_' + Math.random(), tag),
    addEventListener: (t, fn) => { (docListeners[t] = docListeners[t] || []).push(fn); },
    removeEventListener: (t, fn) => { if (docListeners[t]) docListeners[t] = docListeners[t].filter(f => f !== fn); },
    dispatchEvent: (evt) => { (docListeners[evt.type] || []).forEach(fn => fn(evt)); return true; },
    body: makeEl('body')
  };
  global.window = win; global.document = doc; global.CustomEvent = CustomEvent;
  global.HTMLElement = HTMLElement; global.Image = Image;
  global.requestAnimationFrame = (fn) => 1; global.cancelAnimationFrame = () => {};
  return { elMap, win, doc, winListeners };
}

(async () => {
  try {
    const env = setupDOM();
    zJ
  } catch (err) {
    console.error(err);
    process.exit(1);
  }
})();
Únodez--input-type=modulez-eT)Úcapture_outputÚtextÚcwdr   zNode execution failed:
ú
)Ú
subprocessÚrunr   ÚPROJECT_ROOTÚ
returncodeÚRuntimeErrorÚstderrÚstdoutÚjsonÚloadsÚstrip)r   Ú	bootstrapÚress      ú:C:\Projects\FreeExile\tests\unit\test_client_ui_modules.pyÚrun_node_harnessr      s¤   € ð4ð\ ð]4ð 4ð 4€Iõj Œ.Ø	Ð&¨¨iÐ8Ø $­CµÑ,=Ô,=ðñ ô €Cð „~˜ÒÐÝÐP°c´jÐPÐPÀCÄJÐPÐPÑQÔQÐQÝŒ:�c”j×&Ò&Ñ(Ô(Ñ)Ô)Ð)ó    c                  ó   — e Zd Zdd„Zdd„ZdS )ÚTestClientPartyUIr   ÚNonec                óü   — d}t          |¦  «        }|d         du sJ ‚|d         du sJ ‚|d         du sJ ‚|d         du sJ ‚|d         du sJ ‚|d         d	         d
k    sJ ‚|d         d         d         dk    sJ ‚d S )Nuù  
        const { PartyUI } = await import('./client/webapp/js/ui/party_ui.js');
        const partyUI = new PartyUI({ currentUserId: 'p1' });
        partyUI.init();
        const partyState = {
          party_id: 'party_101', leader_id: 'p1', loot_allocation_mode: 'SHORT',
          members: [
            { player_id: 'p1', character_name: 'HeroOne', level: 50, hp: 100, max_hp: 100, mana: 50, max_mana: 50, role: 'LEADER', is_online: true },
            { player_id: 'p2', character_name: 'BuddyTwo', level: 48, hp: 80, max_hp: 100, mana: 40, max_mana: 50, role: 'MEMBER', is_online: true },
            { player_id: 'p3', character_name: 'MageThree', level: 45, hp: 60, max_hp: 100, mana: 50, max_mana: 50, role: 'MEMBER', is_online: false }
          ]
        };
        partyUI.updateParty(partyState);
        const isLeader = partyUI.isLeader();
        const html = partyUI.container.innerHTML;
        let actionDispatched = null;
        partyUI.onActionCallback = (action, payload) => { actionDispatched = { action, payload }; };
        partyUI.setLootMode('PERMANENT');
        const lootAction = actionDispatched;
        partyUI.kickMember('p3');
        const kickAction = actionDispatched;
        console.log(JSON.stringify({
          isLeader, hasPartyTitle: html.includes('ðŸ‘¥'), hasLootBadge: html.includes('party-loot-badge'),
          hasFramesList: html.includes('party-frames-list'), hasLeaderControls: html.includes('party-leader-controls'),
          lootAction, kickAction
        }));
        ÚisLeaderTÚhasPartyTitleÚhasLootBadgeÚhasFramesListÚhasLeaderControlsÚ
lootActionÚactionÚset_loot_modeÚ
kickActionÚpayloadÚ	member_idÚp3©r   ©ÚselfÚscriptÚouts      r   Ú+test_party_ui_lifecycle_and_leader_controlsz=TestClientPartyUI.test_party_ui_lifecycle_and_leader_controlsQ   sÇ   € ðˆõ6 ˜vÑ&Ô&ˆØ�:Œ $Ð&Ð&Ð&Ð&Ø�?Ô# tÐ+Ð+Ð+Ð+Ø�>Ô" dÐ*Ð*Ð*Ð*Ø�?Ô# tÐ+Ð+Ð+Ð+ØÐ&Ô'¨4Ð/Ð/Ð/Ð/Ø�<Ô  Ô*¨oÒ=Ð=Ð=Ð=Ø�<Ô  Ô+¨KÔ8¸DÒ@Ð@Ð@Ð@Ð@Ð@r   c                óp   — d}t          |¦  «        }|d         du sJ ‚|d         du sJ ‚|d         du sJ ‚d S )Na   
        const { PartyUI } = await import('./client/webapp/js/ui/party_ui.js');
        const partyUI = new PartyUI({ currentUserId: 'p2' });
        partyUI.init();
        const partyState = {
          party_id: 'party_102', leader_id: 'p1', loot_allocation_mode: 'FREE_FOR_ALL',
          members: [
            { player_id: 'p1', character_name: 'Leader', role: 'LEADER' },
            { player_id: 'p2', character_name: 'MemberMe', role: 'MEMBER' }
          ]
        };
        partyUI.updateParty(partyState);
        console.log(JSON.stringify({
          isLeader: partyUI.isLeader(),
          hasLeaderControls: partyUI.container.innerHTML.includes('party-leader-controls'),
          hasMemberControls: partyUI.container.innerHTML.includes('party-member-controls')
        }));
        r"   Fr&   ÚhasMemberControlsTr.   r/   s      r   Ú/test_party_ui_member_view_lacks_leader_controlszATestClientPartyUI.test_party_ui_member_view_lacks_leader_controlsv   sa   € ðˆõ$ ˜vÑ&Ô&ˆØ�:Œ %Ð'Ð'Ð'Ð'ØÐ&Ô'¨5Ð0Ð0Ð0Ð0ØÐ&Ô'¨4Ð/Ð/Ð/Ð/Ð/Ð/r   N©r   r    )Ú__name__Ú
__module__Ú__qualname__r3   r6   © r   r   r   r   P   sA   € € € € € ð#Að #Að #Að #AðJ0ð 0ð 0ð 0ð 0ð 0r   r   c                  ó   — e Zd Zdd„Zdd„ZdS )ÚTestClientSocialUIr   r    c                ó¤   — d}t          |¦  «        }|d         du sJ ‚|d         dk    sJ ‚|d         du sJ ‚|d         du sJ ‚|d         du sJ ‚d S )	Nu™  
        const { SocialUI } = await import('./client/webapp/js/ui/social_ui.js');
        const socialUI = new SocialUI({ currentUserId: 'p1' });
        socialUI.init();
        socialUI.open();
        const friends = [
          { player_id: 'f1', character_name: 'Alice', level: 60, is_online: true, zone_name: 'TÃ ng Kiáº¿m Nhai', note: 'Best Archer' },
          { player_id: 'f2', character_name: 'Bob', level: 30, is_online: false, zone_name: 'Doanh Tráº¡i' }
        ];
        socialUI.updatePresence(friends);
        const friendsHtml = socialUI.container.innerHTML;
        socialUI.switchTab('guild');
        const guildMembers = [
          { player_id: 'g1', character_name: 'GuildMaster', rank: 'LEADER', level: 85, is_online: true, zone_name: 'SÃ o Huyá»‡t' }
        ];
        socialUI.updateGuildRoster(guildMembers);
        const guildHtml = socialUI.container.innerHTML;
        console.log(JSON.stringify({
          isOpen: socialUI.isOpen, activeTab: socialUI.activeTab,
          hasAlice: friendsHtml.includes('Alice'), hasNote: friendsHtml.includes('Best Archer'),
          hasGuildMaster: guildHtml.includes('GuildMaster')
        }));
        ÚisOpenTÚ	activeTabÚguildÚhasAliceÚhasNoteÚhasGuildMasterr.   r/   s      r   Ú)test_social_ui_tabs_and_presence_trackingz<TestClientSocialUI.test_social_ui_tabs_and_presence_tracking�   sŠ   € ðˆõ. ˜vÑ&Ô&ˆØ�8Œ} Ð$Ð$Ð$Ð$Ø�;Ô 7Ò*Ð*Ð*Ð*Ø�:Œ $Ð&Ð&Ð&Ð&Ø�9Œ~ Ð%Ð%Ð%Ð%ØÐ#Ô$¨Ð,Ð,Ð,Ð,Ð,Ð,r   c                ó”   — d}t          |¦  «        }|d         d         dk    sJ ‚|d         d         dk    sJ ‚|d         dk    sJ ‚d S )	Na¸  
        const { SocialUI } = await import('./client/webapp/js/ui/social_ui.js');
        const socialUI = new SocialUI({ currentUserId: 'p1' });
        socialUI.init();
        let tpDetail = null;
        window.addEventListener('freeexile:teleportRequest', (e) => { tpDetail = e.detail; });
        socialUI.updatePresence([{ player_id: 'f1', character_name: 'Alice', level: 60, is_online: true }]);
        socialUI.teleportTo('f1', 'FRIEND');
        socialUI.sendFriendRequest('charlie');
        socialUI.saveFriendNote('f1', 'Updated Note');
        const f1Note = socialUI.friends.find(f => f.player_id === 'f1').note;
        console.log(JSON.stringify({ tpDetail, f1Note }));
        ÚtpDetailÚtargetIdÚf1Ú
targetTypeÚFRIENDÚf1NotezUpdated Noter.   r/   s      r   Ú3test_social_ui_friend_actions_and_teleport_dispatchzFTestClientSocialUI.test_social_ui_friend_actions_and_teleport_dispatch¯   sg   € ðˆõ ˜vÑ&Ô&ˆØ�:Œ˜zÔ*¨dÒ2Ð2Ð2Ð2Ø�:Œ˜|Ô,°Ò8Ð8Ð8Ð8Ø�8Œ} Ò.Ð.Ð.Ð.Ð.Ð.r   Nr7   )r8   r9   r:   rE   rM   r;   r   r   r=   r=   �   s<   € € € € € ð-ð -ð -ð -ð>/ð /ð /ð /ð /ð /r   r=   c                  ó   — e Zd Zdd„Zdd„ZdS )ÚTestClientTeleportHUDr   r    c                ó¸   — d}t          |¦  «        }|d         du sJ ‚|d         du sJ ‚|d         du sJ ‚|d         du sJ ‚|d         du sJ ‚|d	         du sJ ‚d S )
Nag  
        const { TeleportHUD } = await import('./client/webapp/js/ui/teleport_hud.js');
        const hud = new TeleportHUD({ durationMs: 3500, movementThreshold: 0.05 });
        hud.init();
        hud.startChannel({ targetId: 'p2', targetName: 'Buddy' });
        const startActive = hud.isChanneling;
        // Sub-threshold movement (0.04 <= 0.05) should NOT cancel
        const moveIgnored = hud.onPlayerMoved(0.04);
        const stillChanneling = hud.isChanneling;
        // Movement over threshold (0.06 > 0.05) cancels immediately
        const moveCancelled = hud.onPlayerMoved(0.06);
        const cancelledState = hud.isChanneling;
        const msg = hud.statusMessage;
        console.log(JSON.stringify({
          startActive, moveIgnored, stillChanneling, moveCancelled, cancelledState,
          hasInterruptMsg: msg.length > 0
        }));
        ÚstartActiveTÚmoveIgnoredFÚstillChannelingÚmoveCancelledÚcancelledStateÚhasInterruptMsgr.   r/   s      r   Ú0test_teleport_channel_and_subthreshold_tolerancezFTestClientTeleportHUD.test_teleport_channel_and_subthreshold_toleranceÄ   s¥   € ðˆõ$ ˜vÑ&Ô&ˆØ�=Ô! TÐ)Ð)Ð)Ð)Ø�=Ô! UÐ*Ð*Ð*Ð*ØÐ$Ô%¨Ð-Ð-Ð-Ð-Ø�?Ô# tÐ+Ð+Ð+Ð+ØÐ#Ô$¨Ð-Ð-Ð-Ð-ØÐ$Ô%¨Ð-Ð-Ð-Ð-Ð-Ð-r   c                óX   — d}t          |¦  «        }|d         du sJ ‚|d         du sJ ‚d S )Naö  
        const { TeleportHUD } = await import('./client/webapp/js/ui/teleport_hud.js');
        const hud = new TeleportHUD({ durationMs: 3500 });
        hud.init();
        hud.startChannel({ targetName: 'Hideout' });
        hud.onPlayerDamaged(25);
        const damagedState = hud.isChanneling;
        hud.startChannel({ targetName: 'Friend' });
        hud.cancelManual();
        const manualState = hud.isChanneling;
        console.log(JSON.stringify({ damagedState, manualState }));
        ÚdamagedStateFÚmanualStater.   r/   s      r   Ú&test_teleport_damage_and_manual_cancelz<TestClientTeleportHUD.test_teleport_damage_and_manual_cancelß   sJ   € ðˆõ ˜vÑ&Ô&ˆØ�>Ô" eÐ+Ð+Ð+Ð+Ø�=Ô! UÐ*Ð*Ð*Ð*Ð*Ð*r   Nr7   )r8   r9   r:   rW   r[   r;   r   r   rO   rO   Ã   s<   € € € € € ð.ð .ð .ð .ð6+ð +ð +ð +ð +ð +r   rO   c                  ó   — e Zd Zdd„Zdd„ZdS )ÚTestClientTownPortalControllerr   r    c                óÌ   — d}t          |¦  «        }|d         du sJ ‚|d         dk    sJ ‚|d         dk    sJ ‚|d         d	k    sJ ‚|d
         dk    sJ ‚|d         dk    sJ ‚d S )Na}  
        const { TownPortalController } = await import('./client/webapp/js/engine/town_portal_controller.js');
        const controller = new TownPortalController();
        controller.init();
        const createRes = controller.requestTownPortal('hero', 'zone_tang_kiem_nhai', 'inst_1', 120.5, 340.5);
        const pId = createRes.portalId;
        const portal = controller.portals.get(pId);
        const trav1 = controller.traversePortal(pId, { id: 'hero', zoneId: 'zone_tang_kiem_nhai' });
        const trav2 = controller.traversePortal(pId, { id: 'hero', zoneId: 'zone_player_hideout' });
        console.log(JSON.stringify({
          success: createRes.success, originZone: portal.originZone, originX: portal.originX,
          hubZone: portal.hubZone, trav1TargetZone: trav1.targetZone,
          trav2TargetZone: trav2.targetZone, trav2TargetX: trav2.targetX
        }));
        ÚsuccessTÚ
originZoneÚzone_tang_kiem_nhaiÚoriginXg      ^@Útrav1TargetZoneÚzone_player_hideoutÚtrav2TargetZoneÚtrav2TargetXr.   r/   s      r   Ú'test_portal_2way_creation_and_traversalzFTestClientTownPortalController.test_portal_2way_creation_and_traversalò   s¥   € ðˆõ ˜vÑ&Ô&ˆØ�9Œ~ Ð%Ð%Ð%Ð%Ø�<Ô Ð$9Ò9Ð9Ð9Ð9Ø�9Œ~ Ò&Ð&Ð&Ð&ØÐ$Ô%Ð)>Ò>Ð>Ð>Ð>ØÐ$Ô%Ð)>Ò>Ð>Ð>Ð>Ø�>Ô" eÒ+Ð+Ð+Ð+Ð+Ð+r   c                óÄ   — d}t          |¦  «        }|d         du sJ ‚|d         dk    sJ ‚|d         du sJ ‚|d         dk    sJ ‚|d	         du sJ ‚|d
         dk    sJ ‚d S )Naµ  
        const { TownPortalController } = await import('./client/webapp/js/engine/town_portal_controller.js');
        const controller = new TownPortalController();
        controller.init();
        controller.setQuarantineState('zone_boss', 'inst_b1', { bossActive: true });
        const bossRes = controller.requestTownPortal('hero', 'zone_boss', 'inst_b1');
        controller.setQuarantineState('zone_puzzle', 'inst_s1', { secretActive: true });
        const secretRes = controller.requestTownPortal('hero', 'zone_puzzle', 'inst_s1');
        controller.registerMapDevice('map_empty', 0);
        const mapRes = controller.requestTownPortal('hero', 'zone_wild', 'map_empty');
        console.log(JSON.stringify({
          bossAllowed: bossRes.success, bossReason: bossRes.reason,
          secretAllowed: secretRes.success, secretReason: secretRes.reason,
          mapAllowed: mapRes.success, mapReason: mapRes.reason
        }));
        ÚbossAllowedFÚ
bossReasonÚQUARANTINE_BOSS_ENGAGEDÚsecretAllowedÚsecretReasonÚQUARANTINE_SECRET_CHAMBERÚ
mapAllowedÚ	mapReasonÚ
MAP_SEALEDr.   r/   s      r   Ú,test_portal_anti_exploit_quarantine_lockoutszKTestClientTownPortalController.test_portal_anti_exploit_quarantine_lockouts
  s¤   € ðˆõ  ˜vÑ&Ô&ˆØ�=Ô! UÐ*Ð*Ð*Ð*Ø�<Ô Ð$=Ò=Ð=Ð=Ð=Ø�?Ô# uÐ,Ð,Ð,Ð,Ø�>Ô"Ð&AÒAÐAÐAÐAØ�<Ô  EÐ)Ð)Ð)Ð)Ø�;Ô <Ò/Ð/Ð/Ð/Ð/Ð/r   Nr7   )r8   r9   r:   rg   rr   r;   r   r   r]   r]   ñ   s<   € € € € € ð,ð ,ð ,ð ,ð00ð 0ð 0ð 0ð 0ð 0r   r]   c                  ó   — e Zd Zdd„ZdS )ÚTestClientI18nCatalogParityr   r    c                óx  — d}t          |¦  «        }|d         }|dk    sJ ‚|d                              ¦   «         D ]}\  }}|d         |k    sJ d|› d�¦   «         ‚t          |d         ¦  «        d	k    sJ d|› d
|d         › �¦   «         ‚t          |d         ¦  «        d	k    sJ d|› d|d         › �¦   «         ‚Œ~d S )Na½  
        const { PARTY_SOCIAL_I18N_CATALOG } = await import('./client/webapp/js/data/i18n.js');
        const locales = ['vi', 'en', 'zh', 'ja', 'ko', 'th', 'de', 'ru', 'es'];
        const viKeys = Object.keys(PARTY_SOCIAL_I18N_CATALOG.vi || {}).sort();
        const parity = {};
        for (const loc of locales) {
          const locKeys = Object.keys(PARTY_SOCIAL_I18N_CATALOG[loc] || {}).sort();
          const missing = viKeys.filter(k => !locKeys.includes(k));
          const extra = locKeys.filter(k => !viKeys.includes(k));
          parity[loc] = { keyCount: locKeys.length, missing, extra };
        }
        console.log(JSON.stringify({ viKeyCount: viKeys.length, parity }));
        Ú
viKeyCounté(   ÚparityÚkeyCountzLocale z key count mismatchÚmissingr   z missing keys: Úextraz extra keys: )r   ÚitemsÚlen)r0   r1   r2   Ú
total_keysÚlocÚinfos         r   Ú&test_9_language_parity_across_all_keyszBTestClientI18nCatalogParity.test_9_language_parity_across_all_keys%  s  € ðˆõ ˜vÑ&Ô&ˆØ˜Ô&ˆ
Ø˜RÒÐÐÐØ˜Xœ×,Ò,Ñ.Ô.ð 	Xð 	X‰IˆC�Ø˜
Ô# zÒ1Ð1Ð1Ð3U¸SÐ3UÐ3UÐ3UÑ1Ô1Ð1Ý�t˜I”Ñ'Ô'¨1Ò,Ð,Ð,Ð.]¸Ð.]Ð.]ÈDÐQZÌOÐ.]Ð.]Ñ,Ô,Ð,Ý�t˜G”}Ñ%Ô%¨Ò*Ð*Ð*Ð,W°cÐ,WÐ,WÈÈWÌÐ,WÐ,WÑ*Ô*Ð*Ð*ð	Xð 	Xr   Nr7   )r8   r9   r:   r�   r;   r   r   rt   rt   $  s.   € € € € € ðXð Xð Xð Xð Xð Xr   rt   )r   r   r   r   )Ú__doc__Ú
__future__r   r   r   Úpathlibr   ÚpytestÚ__file__ÚresolveÚparentr   r   r   r=   rO   r]   rt   r;   r   r   ú<module>r‰      sj  ððð ð #Ð "Ð "Ð "Ð "Ð "Ø €€€Ø Ð Ð Ð Ø Ð Ð Ð Ð Ð Ø €€€àˆt�H‰~Œ~×%Ò%Ñ'Ô'Ô.Ô5Ô<€ð=*ð =*ð =*ð =*ð@<0ð <0ð <0ð <0ð <0ñ <0ô <0ð <0ð~1/ð 1/ð 1/ð 1/ð 1/ñ 1/ô 1/ð 1/ðh++ð ++ð ++ð ++ð ++ñ ++ô ++ð ++ð\00ð 00ð 00ð 00ð 00ñ 00ô 00ð 00ðfXð Xð Xð Xð Xñ Xô Xð Xð Xð Xr   