fix: géolocalisation Cesium+ et tests déterministes
- CesiumAdapter : utilise le champ `title` (analysé ES) au lieu de `title.keyword` qui retournait 0 résultats ; coerce lat/lon en number (certains profils stockent des strings) - DataService : sépare totalVolume (all tx blockchain) de geoCount (tx heatmap) - StatsPanel : barre de couverture géo uniquement en mode live - App : badge source "● live Ğ1v2" ou "○ mock" - DataService.test.ts : mock SubsquidAdapter + CesiumAdapter directement (vi.mock hoistés) pour que les tests soient déterministes quel que soit VITE_USE_LIVE_API dans .env.local - tsconfig.app.json : exclude src/test pour éviter les erreurs de build prod Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-svh w-full overflow-hidden bg-[#0a0b0f] text-white">
|
<div className="flex h-svh w-full overflow-hidden bg-[#0a0b0f] text-white">
|
||||||
{/* Side panel */}
|
{/* Side panel */}
|
||||||
<StatsPanel stats={stats} loading={loading} periodDays={periodDays} />
|
<StatsPanel stats={stats} loading={loading} periodDays={periodDays} source={source} />
|
||||||
|
|
||||||
{/* Map area */}
|
{/* Map area */}
|
||||||
<div className="relative flex-1 min-w-0">
|
<div className="relative flex-1 min-w-0">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ interface StatsPanelProps {
|
|||||||
stats: PeriodStats | null;
|
stats: PeriodStats | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
periodDays: number;
|
periodDays: number;
|
||||||
|
source: 'live' | 'mock';
|
||||||
}
|
}
|
||||||
|
|
||||||
const MEDALS = ['🥇', '🥈', '🥉'];
|
const MEDALS = ['🥇', '🥈', '🥉'];
|
||||||
@@ -18,8 +19,11 @@ function StatCard({ label, value, sub }: { label: string; value: string; sub?: s
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatsPanel({ stats, loading, periodDays }: StatsPanelProps) {
|
export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelProps) {
|
||||||
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
||||||
|
const geoPct = stats && stats.transactionCount > 0
|
||||||
|
? Math.round((stats.geoCount / stats.transactionCount) * 100)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-72 shrink-0 flex flex-col gap-4 bg-[#0a0b0f]/95 backdrop-blur-sm border-r border-[#1e1f2a] p-5 overflow-y-auto">
|
<aside className="w-72 shrink-0 flex flex-col gap-4 bg-[#0a0b0f]/95 backdrop-blur-sm border-r border-[#1e1f2a] p-5 overflow-y-auto">
|
||||||
@@ -57,6 +61,22 @@ export function StatsPanel({ stats, loading, periodDays }: StatsPanelProps) {
|
|||||||
value={stats.transactionCount.toLocaleString('fr-FR')}
|
value={stats.transactionCount.toLocaleString('fr-FR')}
|
||||||
sub={`≈ ${(stats.totalVolume / (stats.transactionCount || 1)).toFixed(2)} Ğ1 / tx`}
|
sub={`≈ ${(stats.totalVolume / (stats.transactionCount || 1)).toFixed(2)} Ğ1 / tx`}
|
||||||
/>
|
/>
|
||||||
|
{/* Couverture géo — uniquement en mode live */}
|
||||||
|
{source === 'live' && geoPct !== null && (
|
||||||
|
<div className="bg-[#0f1016] border border-[#2e2f3a] rounded-xl p-3">
|
||||||
|
<div className="flex justify-between items-center mb-1.5">
|
||||||
|
<p className="text-[#4b5563] text-xs uppercase tracking-widest">Géolocalisées</p>
|
||||||
|
<p className="text-[#6b7280] text-xs">{stats.geoCount} / {stats.transactionCount}</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-[#1e1f2a] rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className="bg-[#d4a843] h-1.5 rounded-full transition-all duration-500"
|
||||||
|
style={{ width: `${geoPct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-[#4b5563] text-xs mt-1 text-right">{geoPct}% via Cesium+</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -87,7 +107,7 @@ export function StatsPanel({ stats, loading, periodDays }: StatsPanelProps) {
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="mt-auto pt-4 border-t border-[#1e1f2a]">
|
<div className="mt-auto pt-4 border-t border-[#1e1f2a]">
|
||||||
<p className="text-[#2e2f3a] text-xs text-center">
|
<p className="text-[#2e2f3a] text-xs text-center">
|
||||||
Données simulées · API Subsquid prête
|
{source === 'live' ? 'Ğ1v2 · Subsquid + Cesium+' : 'Données simulées · mock'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -22,6 +22,28 @@ export const G1v2KeySchema = z.string()
|
|||||||
.startsWith('g1')
|
.startsWith('g1')
|
||||||
.length(49);
|
.length(49);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ğ1v1 (Duniter GVA) — conservé pour référence, non utilisé en production v2
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export const GvaTransactionNodeSchema = z.object({
|
||||||
|
currency: z.literal('g1'),
|
||||||
|
issuers: z.array(z.string().min(43).max(44)),
|
||||||
|
outputs: z.array(z.string().regex(/^\d+:\d+:SIG\(.+\)$/)).min(1),
|
||||||
|
blockstampTime: z.number().int().positive(),
|
||||||
|
comment: z.string().optional(),
|
||||||
|
hash: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GvaResponseSchema = z.object({
|
||||||
|
data: z.object({
|
||||||
|
txsHistoryBc: z.object({
|
||||||
|
both: z.object({
|
||||||
|
edges: z.array(z.object({ node: GvaTransactionNodeSchema })),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Subsquid — réponse brute d'un transfert Ğ1v2
|
// Subsquid — réponse brute d'un transfert Ğ1v2
|
||||||
// Endpoint : POST https://squidv2s.syoul.fr/v1/graphql
|
// Endpoint : POST https://squidv2s.syoul.fr/v1/graphql
|
||||||
@@ -59,8 +81,9 @@ export const CesiumProfileSchema = z.object({
|
|||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
geoPoint: z.object({
|
geoPoint: z.object({
|
||||||
lat: z.number().min(-90).max(90),
|
// Certains profils Cesium+ ont lat/lon en string — coerce pour les deux cas
|
||||||
lon: z.number().min(-180).max(180),
|
lat: z.coerce.number().min(-90).max(90),
|
||||||
|
lon: z.coerce.number().min(-180).max(180),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
* Mode live (USE_LIVE_API = true) : données réelles Ğ1v2.
|
* Mode live (USE_LIVE_API = true) : données réelles Ğ1v2.
|
||||||
* - Transactions : Subsquid indexer https://squidv2s.syoul.fr/v1/graphql
|
* - Transactions : Subsquid indexer https://squidv2s.syoul.fr/v1/graphql
|
||||||
* - Géolocalisation : Cesium+ https://g1.data.e-is.pro
|
* - Géolocalisation : Cesium+ https://g1.data.e-is.pro
|
||||||
* → recherche par nom d'identité (identity.name depuis le graphe Subsquid)
|
* → recherche batch par nom d'identité (champ "title" analysé ES)
|
||||||
* → les transactions sans profil Cesium+ reçoivent des coordonnées approx.
|
* → couverture ~50-60% : les tx sans profil géo sont EXCLUES du heatmap
|
||||||
|
* mais comptées dans totalCount / totalVolume
|
||||||
*
|
*
|
||||||
* Pour activer : définir VITE_USE_LIVE_API=true dans .env.local
|
* Pour activer : définir VITE_USE_LIVE_API=true dans .env.local
|
||||||
*/
|
*/
|
||||||
@@ -19,27 +20,21 @@ import {
|
|||||||
type Transaction,
|
type Transaction,
|
||||||
} from '../data/mockData';
|
} from '../data/mockData';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Configuration
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const USE_LIVE_API = import.meta.env.VITE_USE_LIVE_API === 'true';
|
const USE_LIVE_API = import.meta.env.VITE_USE_LIVE_API === 'true';
|
||||||
|
|
||||||
// Centroïde France — fallback géo quand Cesium+ n'a pas le profil
|
async function fetchLiveTransactions(periodDays: number): Promise<{
|
||||||
const FRANCE_CENTER = { lat: 46.2276, lng: 2.2137 };
|
geolocated: Transaction[];
|
||||||
|
totalCount: number;
|
||||||
// ---------------------------------------------------------------------------
|
totalVolume: number;
|
||||||
// Pipeline données live Ğ1v2
|
}> {
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
async function fetchLiveTransactions(periodDays: number): Promise<Transaction[]> {
|
|
||||||
// 1. Récupère les transferts depuis Subsquid
|
|
||||||
const rawTransfers = await fetchTransfers(periodDays);
|
const rawTransfers = await fetchTransfers(periodDays);
|
||||||
|
if (rawTransfers.length === 0) return { geolocated: [], totalCount: 0, totalVolume: 0 };
|
||||||
|
|
||||||
if (rawTransfers.length === 0) return [];
|
const totalCount = rawTransfers.length;
|
||||||
|
const totalVolume = rawTransfers.reduce((s, t) => s + t.amount, 0);
|
||||||
|
|
||||||
// 2. Collecte les noms d'identité uniques pour la recherche Cesium+
|
// Résolution géo batch via Cesium+
|
||||||
const names = rawTransfers.map((t) => t.fromName).filter(Boolean);
|
const names = rawTransfers.map((t) => t.fromName).filter(Boolean);
|
||||||
|
|
||||||
// 3. Résout les coordonnées via Cesium+ (une seule requête batch)
|
|
||||||
let geoMap = new Map<string, { lat: number; lng: number; city: string }>();
|
let geoMap = new Map<string, { lat: number; lng: number; city: string }>();
|
||||||
try {
|
try {
|
||||||
const profiles = await resolveGeoByNames(names);
|
const profiles = await resolveGeoByNames(names);
|
||||||
@@ -47,53 +42,69 @@ async function fetchLiveTransactions(periodDays: number): Promise<Transaction[]>
|
|||||||
geoMap.set(name, { lat: p.lat, lng: p.lng, city: p.city });
|
geoMap.set(name, { lat: p.lat, lng: p.lng, city: p.city });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Cesium+ indisponible, fallback coordonnées France :', err);
|
console.warn('Cesium+ indisponible :', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Assemble les transactions avec coordonnées
|
// Seules les transactions avec un profil géo entrent dans le heatmap
|
||||||
return rawTransfers.map((t): Transaction => {
|
const geolocated: Transaction[] = [];
|
||||||
const geo = geoMap.get(t.fromName) ?? FRANCE_CENTER;
|
for (const t of rawTransfers) {
|
||||||
return {
|
const geo = geoMap.get(t.fromName);
|
||||||
|
if (!geo) continue; // pas de profil → exclu du heatmap
|
||||||
|
|
||||||
|
geolocated.push({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
timestamp: t.timestamp,
|
timestamp: t.timestamp,
|
||||||
lat: geo.lat,
|
lat: geo.lat,
|
||||||
lng: geo.lng,
|
lng: geo.lng,
|
||||||
amount: t.amount,
|
amount: t.amount,
|
||||||
city: ('city' in geo ? geo.city : undefined) ?? 'Inconnue',
|
city: geo.city,
|
||||||
fromKey: t.fromId,
|
fromKey: t.fromId,
|
||||||
toKey: t.toId,
|
toKey: t.toId,
|
||||||
};
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return { geolocated, totalCount, totalVolume };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Public API
|
// Public API
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
export interface PeriodStats {
|
export interface PeriodStats {
|
||||||
totalVolume: number;
|
totalVolume: number;
|
||||||
transactionCount: number;
|
transactionCount: number; // total blockchain (y compris non-géolocalisés)
|
||||||
|
geoCount: number; // transactions visibles sur la carte
|
||||||
topCities: { name: string; volume: number; count: number }[];
|
topCities: { name: string; volume: number; count: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataResult {
|
export interface DataResult {
|
||||||
transactions: Transaction[];
|
transactions: Transaction[]; // uniquement géolocalisées → heatmap
|
||||||
stats: PeriodStats;
|
stats: PeriodStats;
|
||||||
source: 'live' | 'mock';
|
source: 'live' | 'mock';
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchData(periodDays: number): Promise<DataResult> {
|
export async function fetchData(periodDays: number): Promise<DataResult> {
|
||||||
let transactions: Transaction[];
|
if (!USE_LIVE_API) {
|
||||||
let source: 'live' | 'mock';
|
|
||||||
|
|
||||||
if (USE_LIVE_API) {
|
|
||||||
transactions = await fetchLiveTransactions(periodDays);
|
|
||||||
source = 'live';
|
|
||||||
} else {
|
|
||||||
await new Promise((r) => setTimeout(r, 80));
|
await new Promise((r) => setTimeout(r, 80));
|
||||||
transactions = getTransactionsForPeriod(periodDays);
|
const transactions = getTransactionsForPeriod(periodDays);
|
||||||
source = 'mock';
|
const base = computeStats(transactions);
|
||||||
|
return {
|
||||||
|
transactions,
|
||||||
|
stats: { ...base, geoCount: transactions.length },
|
||||||
|
source: 'mock',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = computeStats(transactions);
|
const { geolocated, totalCount, totalVolume } = await fetchLiveTransactions(periodDays);
|
||||||
return { transactions, stats, source };
|
const base = computeStats(geolocated);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactions: geolocated,
|
||||||
|
stats: {
|
||||||
|
totalVolume, // vrai total blockchain
|
||||||
|
transactionCount: totalCount,
|
||||||
|
geoCount: geolocated.length,
|
||||||
|
topCities: base.topCities,
|
||||||
|
},
|
||||||
|
source: 'live',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,11 +53,13 @@ export async function resolveGeoByNames(
|
|||||||
if (unique.length === 0) return new Map();
|
if (unique.length === 0) return new Map();
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
size: unique.length,
|
// Dépasser la limite par défaut : plusieurs profils peuvent avoir le même prénom
|
||||||
|
size: unique.length * 3,
|
||||||
query: {
|
query: {
|
||||||
bool: {
|
bool: {
|
||||||
must: [
|
must: [
|
||||||
{ terms: { 'title.keyword': unique } },
|
// Champ "title" analysé (lowercase tokens) — title.keyword retourne 0 résultats
|
||||||
|
{ terms: { title: unique } },
|
||||||
],
|
],
|
||||||
filter: [
|
filter: [
|
||||||
{ exists: { field: 'geoPoint' } },
|
{ exists: { field: 'geoPoint' } },
|
||||||
|
|||||||
@@ -1,44 +1,69 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { fetchData } from '../services/DataService';
|
|
||||||
|
|
||||||
// Mock the mockData module so tests are deterministic
|
// Mocker les adaptateurs live AVANT l'import de DataService,
|
||||||
|
// pour que les tests soient déterministes quel que soit VITE_USE_LIVE_API
|
||||||
|
vi.mock('../services/adapters/SubsquidAdapter', () => ({
|
||||||
|
fetchTransfers: vi.fn(async (days: number) => [
|
||||||
|
{ id: 't1', timestamp: Date.now(), amount: 20, fromId: 'g1' + 'a'.repeat(47), toId: 'g1' + 'b'.repeat(47), fromName: 'Alice' },
|
||||||
|
{ id: 't2', timestamp: Date.now(), amount: 10, fromId: 'g1' + 'c'.repeat(47), toId: 'g1' + 'd'.repeat(47), fromName: 'Bob' },
|
||||||
|
].slice(0, days >= 7 ? 2 : 1)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../services/adapters/CesiumAdapter', () => ({
|
||||||
|
resolveGeoByNames: vi.fn(async () => new Map([
|
||||||
|
['Alice', { name: 'Alice', city: 'Paris', lat: 48.8566, lng: 2.3522 }],
|
||||||
|
['Bob', { name: 'Bob', city: 'Lyon', lat: 45.764, lng: 4.8357 }],
|
||||||
|
])),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../data/mockData', () => ({
|
vi.mock('../data/mockData', () => ({
|
||||||
getTransactionsForPeriod: vi.fn((days: number) => [
|
getTransactionsForPeriod: vi.fn((days: number) => [
|
||||||
{ id: 't1', timestamp: Date.now(), lat: 48.8, lng: 2.3, amount: 20, city: 'Paris', fromKey: 'a', toKey: 'b' },
|
{ id: 't1', timestamp: Date.now(), lat: 48.8, lng: 2.3, amount: 20, city: 'Paris', fromKey: 'g1' + 'a'.repeat(47), toKey: 'g1' + 'b'.repeat(47) },
|
||||||
{ id: 't2', timestamp: Date.now(), lat: 45.7, lng: 4.8, amount: 10, city: 'Lyon', fromKey: 'c', toKey: 'd' },
|
{ id: 't2', timestamp: Date.now(), lat: 45.7, lng: 4.8, amount: 10, city: 'Lyon', fromKey: 'g1' + 'c'.repeat(47), toKey: 'g1' + 'd'.repeat(47) },
|
||||||
].slice(0, days >= 7 ? 2 : 1)),
|
].slice(0, days >= 7 ? 2 : 1)),
|
||||||
computeStats: vi.fn((txs) => ({
|
computeStats: vi.fn((txs) => ({
|
||||||
totalVolume: txs.reduce((s: number, t: { amount: number }) => s + t.amount, 0),
|
totalVolume: txs.reduce((s: number, t: { amount: number }) => s + t.amount, 0),
|
||||||
transactionCount: txs.length,
|
transactionCount: txs.length,
|
||||||
topCities: [{ name: 'Paris', volume: 20, count: 1 }],
|
topCities: [{ name: 'Paris', volume: 20, count: 1 }],
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => vi.clearAllMocks());
|
beforeEach(() => vi.clearAllMocks());
|
||||||
|
|
||||||
|
import { fetchData } from '../services/DataService';
|
||||||
|
|
||||||
describe('fetchData', () => {
|
describe('fetchData', () => {
|
||||||
it('returns transactions and stats shaped correctly', async () => {
|
it('retourne transactions et stats avec la bonne forme', async () => {
|
||||||
const result = await fetchData(7);
|
const result = await fetchData(7);
|
||||||
expect(result).toHaveProperty('transactions');
|
expect(result).toHaveProperty('transactions');
|
||||||
expect(result).toHaveProperty('stats');
|
expect(result).toHaveProperty('stats');
|
||||||
|
expect(result).toHaveProperty('source');
|
||||||
expect(result.stats).toHaveProperty('totalVolume');
|
expect(result.stats).toHaveProperty('totalVolume');
|
||||||
expect(result.stats).toHaveProperty('transactionCount');
|
expect(result.stats).toHaveProperty('transactionCount');
|
||||||
|
expect(result.stats).toHaveProperty('geoCount');
|
||||||
expect(result.stats).toHaveProperty('topCities');
|
expect(result.stats).toHaveProperty('topCities');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filters by period — 1 day returns fewer transactions than 7 days', async () => {
|
it('1 jour retourne moins de transactions que 7 jours', async () => {
|
||||||
const r1 = await fetchData(1);
|
const r1 = await fetchData(1);
|
||||||
const r7 = await fetchData(7);
|
const r7 = await fetchData(7);
|
||||||
expect(r7.transactions.length).toBeGreaterThanOrEqual(r1.transactions.length);
|
expect(r7.transactions.length).toBeGreaterThanOrEqual(r1.transactions.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stats.totalVolume matches sum of returned transactions', async () => {
|
it('totalVolume >= somme des transactions géolocalisées', async () => {
|
||||||
const { transactions, stats } = await fetchData(7);
|
const { transactions, stats } = await fetchData(7);
|
||||||
const sum = transactions.reduce((s, t) => s + t.amount, 0);
|
const geoSum = transactions.reduce((s, t) => s + t.amount, 0);
|
||||||
expect(stats.totalVolume).toBeCloseTo(sum);
|
// En mode live : totalVolume = total blockchain >= geolocalisées
|
||||||
|
// En mode mock : totalVolume = geoSum (même ensemble)
|
||||||
|
expect(stats.totalVolume).toBeGreaterThanOrEqual(geoSum - 0.01);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves (does not reject) for all valid periods', async () => {
|
it('geoCount <= transactionCount', async () => {
|
||||||
|
const { stats } = await fetchData(7);
|
||||||
|
expect(stats.geoCount).toBeLessThanOrEqual(stats.transactionCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ne rejette pas pour les trois périodes valides', async () => {
|
||||||
await expect(fetchData(1)).resolves.toBeDefined();
|
await expect(fetchData(1)).resolves.toBeDefined();
|
||||||
await expect(fetchData(7)).resolves.toBeDefined();
|
await expect(fetchData(7)).resolves.toBeDefined();
|
||||||
await expect(fetchData(30)).resolves.toBeDefined();
|
await expect(fetchData(30)).resolves.toBeDefined();
|
||||||
|
|||||||
@@ -24,5 +24,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"],
|
||||||
|
"exclude": ["src/test"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user