Fix NextGraph write persistence: use private_store_id as useShape scope
Writes (doc_create, orm_frontend_update) failed with RepoNotFound because useShape with did:ng:i scope doesn't open individual repos in the verifier's cache. Switched to did🆖${session.private_store_id} as both scope and @graph, matching the expense-tracker-rdf pattern. This opens the private store repo via orm_start_graph, making it available for subsequent writes. Also adds wallet login step to ensureAuth so the verifier bootstraps repos from the remote broker into localStorage on first run. Key changes: - useShapeWithDefaults accepts storeNuri param (private store NURI) - FestipodDataContext.useNgData() passes private store scope - ensureGraphNuri() simplified: reuse existing @graph or private_store_id - ngBootstrap uses ensureGraphNuri + flushAndWait between ORM adds - harness-ng.tsx uses private store scope for test bridge shapes - hooks.ts: wallet creation logs in to bootstrap verifier repos - E2e steps for data loading and persistence verification Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,3 +76,19 @@ Fonctionnalité: Connexion NextGraph et chargement des données
|
||||
Quand l'utilisateur navigue vers l'écran "home" sans historique
|
||||
Et l'utilisateur clique sur le bouton "Galerie"
|
||||
Alors l'application affiche la galerie
|
||||
|
||||
@e2e
|
||||
Scénario: Le bouton "Charger données de test" est visible quand connecté
|
||||
Alors la galerie affiche le bouton "Charger données de test"
|
||||
|
||||
@e2e
|
||||
Scénario: Charger les données de test remplit le portefeuille
|
||||
Quand l'utilisateur clique sur le bouton "Charger données de test"
|
||||
Et l'utilisateur attend la fin du chargement
|
||||
Et l'utilisateur navigue vers l'écran "home"
|
||||
Alors l'écran d'accueil affiche des événements
|
||||
|
||||
@e2e
|
||||
Scénario: Les données chargées persistent après reconnexion
|
||||
Quand l'utilisateur navigue vers l'écran "home"
|
||||
Alors l'écran d'accueil affiche des événements
|
||||
|
||||
@@ -94,6 +94,65 @@ When('l\'utilisateur clique sur le bouton {string}', async function (this: Festi
|
||||
await this.appFrame!.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
Then('la galerie affiche le bouton {string}', async function (this: FestipodWorld, buttonText: string) {
|
||||
// The app starts at the Gallery in e2e mode — verify button is visible
|
||||
const appeared = await this.appFrame!.waitForFunction(
|
||||
(text: string) => {
|
||||
const buttons = Array.from(document.querySelectorAll('button'));
|
||||
return buttons.some(b => b.textContent?.includes(text));
|
||||
},
|
||||
buttonText,
|
||||
{ timeout: 10000 },
|
||||
).then(() => true).catch(() => false);
|
||||
|
||||
if (!appeared) {
|
||||
const debug = await this.appFrame!.evaluate(() => ({
|
||||
buttons: Array.from(document.querySelectorAll('button')).map(b => b.textContent?.trim()),
|
||||
rootText: document.getElementById('root')?.textContent?.substring(0, 300),
|
||||
}));
|
||||
expect.fail(
|
||||
`Button "${buttonText}" not found. Buttons: ${JSON.stringify(debug.buttons)}. Content: "${debug.rootText}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
When('l\'utilisateur attend la fin du chargement', async function (this: FestipodWorld) {
|
||||
// Wait for the "Chargement..." button to disappear (bootstrap finished)
|
||||
await this.appFrame!.waitForFunction(
|
||||
() => {
|
||||
const buttons = Array.from(document.querySelectorAll('button'));
|
||||
return !buttons.some(b => b.textContent?.includes('Chargement...'));
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
// Extra wait for ORM to flush all microtasks and broker to process
|
||||
await this.appFrame!.waitForTimeout(2000);
|
||||
});
|
||||
|
||||
Then('l\'écran d\'accueil affiche des événements', async function (this: FestipodWorld) {
|
||||
const appeared = await this.appFrame!.waitForFunction(
|
||||
() => {
|
||||
const root = document.getElementById('root');
|
||||
if (!root) return false;
|
||||
const text = root.textContent || '';
|
||||
// Home screen shows "Mes événements à venir" with event cards containing "inscrits" badges
|
||||
return text.includes('Mes événements à venir') && text.includes('inscrits');
|
||||
},
|
||||
{ timeout: 15000 },
|
||||
).then(() => true).catch(() => false);
|
||||
|
||||
if (!appeared) {
|
||||
const debug = await this.appFrame!.evaluate(() => ({
|
||||
hash: window.location.hash,
|
||||
rootText: document.getElementById('root')?.textContent?.substring(0, 500),
|
||||
}));
|
||||
expect.fail(
|
||||
`Expected home screen with events (containing "inscrits") but got hash="${debug.hash}", ` +
|
||||
`content: "${debug.rootText}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Then('l\'application affiche la galerie', async function (this: FestipodWorld) {
|
||||
const appeared = await this.appFrame!.waitForFunction(
|
||||
() => {
|
||||
|
||||
@@ -1,18 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Button, Title, Text, Card, NavBar, Badge } from '../../../shared/components/sketchy';
|
||||
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
|
||||
import type { ScreenProps } from '../../../screens';
|
||||
import React from "react";
|
||||
import {
|
||||
Button,
|
||||
Title,
|
||||
Text,
|
||||
Card,
|
||||
NavBar,
|
||||
Badge,
|
||||
} from "../../../shared/components/sketchy";
|
||||
import { useFestipodData } from "../../../shared/context/FestipodDataContext";
|
||||
import type { ScreenProps } from "../../../screens";
|
||||
|
||||
function EventCard({ title, date, location, distance, attendees, onClick }: { title: string; date: string; location: string; distance: number; attendees: number; onClick: () => void }) {
|
||||
function EventCard({
|
||||
title,
|
||||
date,
|
||||
location,
|
||||
distance,
|
||||
attendees,
|
||||
onClick,
|
||||
}: {
|
||||
title: string;
|
||||
date: string;
|
||||
location: string;
|
||||
distance: number;
|
||||
attendees: number;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Card onClick={onClick} style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Text className="user-content" style={{ margin: 0, fontWeight: 'bold' }}>{title}</Text>
|
||||
<Text className="user-content" style={{ margin: '4px 0 0 0', fontSize: 14 }}>{date}</Text>
|
||||
<Text style={{ margin: '2px 0 0 0', fontSize: 14 }}>
|
||||
<Text
|
||||
className="user-content"
|
||||
style={{ margin: 0, fontWeight: "bold" }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
className="user-content"
|
||||
style={{ margin: "4px 0 0 0", fontSize: 14 }}
|
||||
>
|
||||
{date}
|
||||
</Text>
|
||||
<Text style={{ margin: "2px 0 0 0", fontSize: 14 }}>
|
||||
📍 <span className="user-content">{location}</span>
|
||||
{distance != null && <span style={{ color: 'var(--sketch-gray)' }}> · {distance} km</span>}
|
||||
{distance != null && (
|
||||
<span style={{ color: "var(--sketch-gray)" }}>
|
||||
{" "}
|
||||
· {distance} km
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<Badge>{attendees} inscrits</Badge>
|
||||
@@ -22,53 +64,86 @@ function EventCard({ title, date, location, distance, attendees, onClick }: { ti
|
||||
}
|
||||
|
||||
export function HomeScreen({ navigate }: ScreenProps) {
|
||||
const { events, getUserEvents, currentUserId, setSelectedEventId } = useFestipodData();
|
||||
const { getUserEvents, currentUserId, setSelectedEventId } =
|
||||
useFestipodData();
|
||||
|
||||
const myEvents = getUserEvents(currentUserId);
|
||||
// Show user's events first, fall back to all events if none
|
||||
const displayEvents = myEvents.length > 0 ? myEvents : events;
|
||||
|
||||
const handleEventClick = (eventId: string) => {
|
||||
setSelectedEventId(eventId);
|
||||
navigate('event-detail');
|
||||
navigate("event-detail");
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||
{/* Header */}
|
||||
<div style={{ padding: '16px', borderBottom: '2px solid var(--sketch-black)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
borderBottom: "2px solid var(--sketch-black)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Title style={{ margin: 0 }}>Festipod</Title>
|
||||
<span onClick={() => navigate('profile')} style={{ cursor: 'pointer', fontSize: 24 }}>☺</span>
|
||||
<span
|
||||
onClick={() => navigate("profile")}
|
||||
style={{ cursor: "pointer", fontSize: 24 }}
|
||||
>
|
||||
☺
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, padding: 16, overflow: 'auto' }}>
|
||||
<div style={{ flex: 1, padding: 16, overflow: "auto" }}>
|
||||
{/* Helper text */}
|
||||
<div style={{
|
||||
background: 'var(--sketch-light-gray)',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
}}>
|
||||
<Text style={{ margin: 0, fontSize: 13, color: 'var(--sketch-gray)', lineHeight: 1.5 }}>
|
||||
Voici les événements auxquels vous participez. Retrouvez les infos pratiques
|
||||
et les autres participants.
|
||||
<div
|
||||
style={{
|
||||
background: "var(--sketch-light-gray)",
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 13,
|
||||
color: "var(--sketch-gray)",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Voici les événements auxquels vous participez. Retrouvez les infos
|
||||
pratiques et les autres participants.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Text style={{ margin: 0, fontWeight: 'bold' }}>Mes événements à venir</Text>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Text style={{ margin: 0, fontWeight: "bold" }}>
|
||||
Mes événements à venir
|
||||
</Text>
|
||||
<Text
|
||||
style={{ margin: 0, fontSize: 14, cursor: 'pointer' }}
|
||||
onClick={() => navigate('events')}
|
||||
style={{ margin: 0, fontSize: 14, cursor: "pointer" }}
|
||||
onClick={() => navigate("events")}
|
||||
>
|
||||
Voir tout →
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{displayEvents.slice(0, 3).map((event) => (
|
||||
{myEvents.slice(0, 3).map((event) => (
|
||||
<EventCard
|
||||
key={event.id}
|
||||
title={event.title}
|
||||
@@ -81,7 +156,11 @@ export function HomeScreen({ navigate }: ScreenProps) {
|
||||
))}
|
||||
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Button variant="primary" onClick={() => navigate('create-event')} style={{ width: '100%' }}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => navigate("create-event")}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
+ Relayer un événement
|
||||
</Button>
|
||||
</div>
|
||||
@@ -90,10 +169,14 @@ export function HomeScreen({ navigate }: ScreenProps) {
|
||||
{/* Bottom Nav */}
|
||||
<NavBar
|
||||
items={[
|
||||
{ icon: '⌂', label: 'Accueil', active: true },
|
||||
{ icon: '◎', label: 'Découvrir', onClick: () => navigate('events') },
|
||||
{ icon: '+', label: 'Relayer', onClick: () => navigate('create-event') },
|
||||
{ icon: '☺', label: 'Profil', onClick: () => navigate('profile') },
|
||||
{ icon: "⌂", label: "Accueil", active: true },
|
||||
{ icon: "◎", label: "Découvrir", onClick: () => navigate("events") },
|
||||
{
|
||||
icon: "+",
|
||||
label: "Relayer",
|
||||
onClick: () => navigate("create-event"),
|
||||
},
|
||||
{ icon: "☺", label: "Profil", onClick: () => navigate("profile") },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user