import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { test } from 'node:test'; import React from 'react'; import { act, create } from 'react-test-renderer'; import ts from 'typescript'; import * as chatModel from '../src/lib/chat.ts'; globalThis.IS_REACT_ACT_ENVIRONMENT = true; const require = createRequire(import.meta.url); const deferred = () => { let resolve; const promise = new Promise(yes => { resolve = yes; }); return { promise, resolve }; }; test('Eine verspätete Raumliste verdrängt keinen gerade angelegten Projektchat', async () => { const project = { key: 'project_42', name: 'Projekt', source: 'internal', type: 'project', exists: false, projectId: 42 }; const directory = { rooms: [{ key: 'allgemein', name: 'Allgemein', source: 'internal', exists: true }, project], hiddenRoomKeys: [], invitations: [], projects: [] }; const provision = deferred(), list = deferred(), timers = new Map(); let directoryCalls = 0; const api = { directory: () => ++directoryCalls === 1 ? Promise.resolve(directory) : list.promise, provision: () => provision.promise }; const output = ts.transpileModule(readFileSync(new URL('../app/(tabs)/communication.tsx', import.meta.url), 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX, target: ts.ScriptTarget.ES2022 }, }).outputText; const exports = {}; new Function('require', 'exports', 'setInterval', 'clearInterval', output)(name => { if (name === 'react-native') return { ...Object.fromEntries(['ActivityIndicator', 'FlatList', 'KeyboardAvoidingView', 'Modal', 'Pressable', 'ScrollView', 'Text', 'TextInput', 'View'].map(key => [key, key])), StyleSheet: { create: value => value }, Platform: { OS: 'ios' }, DeviceEventEmitter: { emit() {} }, AppState: { currentState: 'active', addEventListener: () => ({ remove() {} }) }, }; if (name === '@expo/vector-icons') return { Ionicons: 'Icon' }; if (name === '@react-navigation/native') return { useIsFocused: () => true }; if (name === 'expo-crypto') return { randomUUID: () => 'draft-id' }; if (name === '@/components/chat-conversation') return { ChatConversation: 'Conversation' }; if (name === '@/src/lib/api') return {}; if (name === '@/src/lib/chat-api') return { chatApi: api }; if (name === '@/src/lib/chat') return chatModel; if (name === '@/src/lib/server-config') return { getApiBaseUrlSync: () => 'https://test.invalid' }; if (name === '@/src/providers/auth-provider') return { useAuth: () => ({ token: 'test', activeTenantId: 1 }) }; return require(name); }, exports, callback => { const id = Symbol(); timers.set(id, callback); return id; }, id => timers.delete(id)); let root; try { await act(async () => { root = create(React.createElement(exports.default)); }); const roomList = root.root.findAllByType('FlatList').find(node => node.props.horizontal); await act(async () => roomList.props.renderItem({ item: project }).props.onPress()); await act(async () => { for (const callback of timers.values()) callback(); }); assert.equal(directoryCalls, 2); await act(async () => provision.resolve({ ...project, exists: true })); assert.equal(root.root.findByType('Conversation').props.room.key, 'project_42'); await act(async () => list.resolve(directory)); assert.equal(root.root.findByType('Conversation').props.room.key, 'project_42'); } finally { await act(async () => root?.unmount()); } });