Ng eventually #1
@@ -19,18 +19,22 @@ Given('le créateur relaie l\'événement {string}', { timeout: 180000 }, async
|
||||
|
||||
Then('le créateur n\'est pas participant de l\'événement {string}', { timeout: 60000 }, async function (this: FestipodWorld, title: string) {
|
||||
const eventId = (this as any).creatorEventId;
|
||||
// Authoritative: the broker must hold 0 participations for (E, creator). Poll a
|
||||
// little to absorb any pending write from a just-run leave.
|
||||
// Wait for the REACTIVE state to settle to "not participating" (observes AD().*
|
||||
// fed by the subscription push — NOT a broker re-read), the sign the sync has
|
||||
// passed. Then do ONE authoritative broker read to prove the 0 is durable.
|
||||
await this.appFrame!.waitForFunction(
|
||||
(eventId) => {
|
||||
const td = (window as any).__testData;
|
||||
const uid = td.currentUserId;
|
||||
return !!uid && !td.isParticipating(eventId, uid);
|
||||
},
|
||||
eventId,
|
||||
{ timeout: 30000 },
|
||||
).catch(() => { /* fall through to the single authoritative read */ });
|
||||
const n = await this.appFrame!.evaluate(async ({ eventId }: { eventId: string }) => {
|
||||
const td = (window as any).__testData;
|
||||
const uid = await td.ensureCurrentUser();
|
||||
let last = -1;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
last = await td.authParticipationCount(eventId, uid);
|
||||
if (last === 0) return 0;
|
||||
await new Promise(r => setTimeout(r, 750));
|
||||
}
|
||||
return last;
|
||||
return td.authParticipationCount(eventId, uid);
|
||||
}, { eventId });
|
||||
expect(n, `creator must NOT be participating in "${title}" (broker count)`).to.equal(0);
|
||||
});
|
||||
@@ -68,16 +72,22 @@ When('le créateur quitte son événement {string}', { timeout: 120000 }, async
|
||||
|
||||
Then('le créateur est participant de l\'événement {string}', { timeout: 60000 }, async function (this: FestipodWorld, title: string) {
|
||||
const eventId = (this as any).creatorEventId;
|
||||
// Wait for the REACTIVE join to settle (observes AD().isParticipating fed by the
|
||||
// subscription push — NOT a broker re-read), then take ONE authoritative broker
|
||||
// count to prove the participation is durable at the data level.
|
||||
await this.appFrame!.waitForFunction(
|
||||
(eventId) => {
|
||||
const td = (window as any).__testData;
|
||||
const uid = td.currentUserId;
|
||||
return !!uid && td.isParticipating(eventId, uid);
|
||||
},
|
||||
eventId,
|
||||
{ timeout: 45000 },
|
||||
).catch(() => { /* fall through to the single authoritative read */ });
|
||||
const n = await this.appFrame!.evaluate(async ({ eventId }: { eventId: string }) => {
|
||||
const td = (window as any).__testData;
|
||||
const uid = await td.ensureCurrentUser();
|
||||
let last = 0;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
last = await td.authParticipationCount(eventId, uid);
|
||||
if (last >= 1) return last;
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
return last;
|
||||
return td.authParticipationCount(eventId, uid);
|
||||
}, { eventId });
|
||||
expect(n, `creator must be participating in "${title}" after joining`).to.equal(1);
|
||||
});
|
||||
|
||||
@@ -88,6 +88,9 @@ Then('l\'utilisateur devient participant de l\'événement {string}', async func
|
||||
// Poll: the participation is written into its own protected doc and read back
|
||||
// reactively; under a busy wallet that read can lag, so accept the AUTHORITATIVE
|
||||
// broker count as well (the write is durable regardless of the reactive re-read).
|
||||
// Wait for the REACTIVE participation to settle (observes AD().liveIsParticipating
|
||||
// fed by the subscription push — NOT a broker re-read). Generous window: this
|
||||
// reactive convergence IS the sync-passed signal.
|
||||
await this.appFrame!.waitForFunction(
|
||||
(title) => {
|
||||
const td = (window as any).__testData;
|
||||
@@ -97,23 +100,16 @@ Then('l\'utilisateur devient participant de l\'événement {string}', async func
|
||||
},
|
||||
eventTitle,
|
||||
{ timeout: 45000 },
|
||||
).catch(async () => {
|
||||
// Reactive read lagged — confirm authoritatively against the broker, polling
|
||||
// to absorb the index-append propagation lag of the per-entity fan-out.
|
||||
const n = await this.appFrame!.evaluate(async (title) => {
|
||||
const td = (window as any).__testData;
|
||||
const event = [...td.events].find((e: any) => e.title === title);
|
||||
if (!event) return 0;
|
||||
const uid = await td.ensureCurrentUser();
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const c = await td.authParticipationCount(event['@id'], uid);
|
||||
if (c > 0) return c;
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
return 0;
|
||||
}, eventTitle);
|
||||
expect(n, `participation to "${eventTitle}" must exist on the broker`).to.be.greaterThan(0);
|
||||
});
|
||||
).catch(() => { /* fall through to a single authoritative confirmation */ });
|
||||
// Then ONE authoritative broker read to prove the participation is durable.
|
||||
const n = await this.appFrame!.evaluate(async (title) => {
|
||||
const td = (window as any).__testData;
|
||||
const event = [...td.events].find((e: any) => e.title === title);
|
||||
if (!event) return 0;
|
||||
const uid = await td.ensureCurrentUser();
|
||||
return td.authParticipationCount(event['@id'], uid);
|
||||
}, eventTitle);
|
||||
expect(n, `participation to "${eventTitle}" must exist on the broker`).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
Then('le broker ne contient plus aucune participation à l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
|
||||
|
||||
@@ -97,6 +97,9 @@ Then('l\'utilisateur est participant de l\'événement {string}', async function
|
||||
// The participation is written into its own protected document and read back
|
||||
// reactively — poll (the read lags the write against the broker), resolving the
|
||||
// current user id at call time.
|
||||
// Wait for the REACTIVE participation to settle (observes AD().isParticipating fed
|
||||
// by the subscription push — NOT a broker re-read). Generous window: this reactive
|
||||
// convergence IS the sync-passed signal.
|
||||
await this.appFrame!.waitForFunction(
|
||||
(title) => {
|
||||
const td = (window as any).__testData;
|
||||
@@ -106,24 +109,18 @@ Then('l\'utilisateur est participant de l\'événement {string}', async function
|
||||
return !!event && td.isParticipating(event['@id'], uid);
|
||||
},
|
||||
eventTitle,
|
||||
{ timeout: 30000 },
|
||||
).catch(async () => {
|
||||
// Reactive read lagged — confirm authoritatively against the broker, polling
|
||||
// to absorb the index-append propagation lag of the per-entity fan-out.
|
||||
const n = await this.appFrame!.evaluate(async (title) => {
|
||||
const td = (window as any).__testData;
|
||||
const event = [...td.events].find((e: any) => e.title === title);
|
||||
if (!event) return 0;
|
||||
const uid = await td.ensureCurrentUser();
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const c = await td.authParticipationCount(event['@id'], uid);
|
||||
if (c > 0) return c;
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
return 0;
|
||||
}, eventTitle);
|
||||
expect(n, `User should be participating in "${eventTitle}"`).to.be.greaterThan(0);
|
||||
});
|
||||
{ timeout: 45000 },
|
||||
).catch(() => { /* fall through to a single authoritative confirmation */ });
|
||||
// Then ONE authoritative broker read (bypasses the reactive set) to prove the
|
||||
// participation is durable at the data level.
|
||||
const n = await this.appFrame!.evaluate(async (title) => {
|
||||
const td = (window as any).__testData;
|
||||
const event = [...td.events].find((e: any) => e.title === title);
|
||||
if (!event) return 0;
|
||||
const uid = await td.ensureCurrentUser();
|
||||
return td.authParticipationCount(event['@id'], uid);
|
||||
}, eventTitle);
|
||||
expect(n, `User should be participating in "${eventTitle}"`).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
Then('l\'utilisateur n\'est plus participant de l\'événement {string}', async function (this: FestipodWorld, eventTitle: string) {
|
||||
@@ -163,17 +160,27 @@ Then('le compteur dérivé de l\'événement {string} reflète l\'inscription',
|
||||
// (The absolute 1→2 convergence stays proven end-to-end by the @multibrowser
|
||||
// reactive scenario with a real owner A + joiner B.) Poll (the deposit's index
|
||||
// append + broker sync lag), bounded.
|
||||
// First wait for the REACTIVE join to settle (observes AD().isParticipating fed by
|
||||
// the subscription push — the sign the deposit's write path has run — NOT a broker
|
||||
// re-read). THEN materialize the owner's derived active set from the broker ONCE.
|
||||
await this.appFrame!.waitForFunction(
|
||||
(title) => {
|
||||
const td = (window as any).__testData;
|
||||
const uid = td.currentUserId;
|
||||
if (!uid) return false;
|
||||
const ev = [...td.events].find((e: any) => e.title === title);
|
||||
return !!ev && td.isParticipating(ev['@id'], uid);
|
||||
},
|
||||
eventTitle,
|
||||
{ timeout: 45000 },
|
||||
).catch(() => { /* fall through to the single authoritative materialization */ });
|
||||
const inActive = await this.appFrame!.evaluate(async (title) => {
|
||||
const td = (window as any).__testData;
|
||||
const ev = [...td.events].find((e: any) => e.title === title);
|
||||
const uid = await td.ensureCurrentUser();
|
||||
if (!ev || !uid) return false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const users: (string | null)[] = await td.activeRegistrationUsers(ev['@id']);
|
||||
if (users.includes(uid)) return true;
|
||||
await new Promise(r => setTimeout(r, 750));
|
||||
}
|
||||
return false;
|
||||
const users: (string | null)[] = await td.activeRegistrationUsers(ev['@id']);
|
||||
return users.includes(uid);
|
||||
}, eventTitle);
|
||||
expect(inActive, `the just-joined user must be in the owner-derived active set for "${eventTitle}"`).to.be.true;
|
||||
});
|
||||
@@ -221,19 +228,27 @@ Then('l\'inscription est idempotente pour l\'événement {string}', async functi
|
||||
// and its index-append propagates async, so a single unpolled read can catch 0
|
||||
// before the write is queryable (observed flake) — poll until the durable state
|
||||
// (exactly 1) is visible, which also proves the second join did NOT add a dupe.
|
||||
// First wait for the REACTIVE participation to settle (observes AD().isParticipating
|
||||
// fed by the subscription push — the sync-passed signal — NOT a broker re-read).
|
||||
// THEN read the AUTHORITATIVE broker count ONCE: exactly 1 proves the second join
|
||||
// added no dupe (bypasses reactive-read lag).
|
||||
await this.appFrame!.waitForFunction(
|
||||
(title) => {
|
||||
const td = (window as any).__testData;
|
||||
const uid = td.currentUserId;
|
||||
if (!uid) return false;
|
||||
const event = [...td.events].find((e: any) => e.title === title);
|
||||
return !!event && td.isParticipating(event['@id'], uid);
|
||||
},
|
||||
eventTitle,
|
||||
{ timeout: 45000 },
|
||||
).catch(() => { /* fall through to the single authoritative count */ });
|
||||
const n = await this.appFrame!.evaluate(async (title) => {
|
||||
const td = (window as any).__testData;
|
||||
const event = [...td.events].find((e: any) => e.title === title);
|
||||
if (!event) return -1;
|
||||
const uid = await td.ensureCurrentUser();
|
||||
let last = 0;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
last = await td.authParticipationCount(event['@id'], uid);
|
||||
if (last === 1) return 1; // exactly one — idempotent, stop early
|
||||
if (last > 1) return last; // a dupe leaked — fail fast with the real count
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
return last;
|
||||
return td.authParticipationCount(event['@id'], uid);
|
||||
}, eventTitle);
|
||||
expect(n, 'User should have exactly one participation record on the broker').to.equal(1);
|
||||
});
|
||||
|
||||
@@ -20,14 +20,18 @@ Given('l\'identité A crée l\'événement {string} et s\'y inscrit', { timeout:
|
||||
const aId = await td.ensureCurrentUser();
|
||||
const created = await td.createEventReal(title);
|
||||
await td.appJoinEvent(created.id, aId);
|
||||
// Wait until A's own participation is in A's reactive set (authoritative-ish).
|
||||
for (let i = 0; i < 30; i++) {
|
||||
if (td.isParticipating(created.id, aId)) break;
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
return { eventId: created.id, aId, aParticipates: td.isParticipating(created.id, aId) };
|
||||
return { eventId: created.id, aId };
|
||||
}, title);
|
||||
expect(out.aParticipates, 'A must be participating in its own event before B arrives').to.be.true;
|
||||
// OBSERVE the REACTIVE set until A's own join has settled (isParticipating reads
|
||||
// AD() fed by the subscription push — NOT a broker re-read). This is the barrier
|
||||
// that A's participation exists before B arrives; no broker polling.
|
||||
const aParticipates = await this.appFrame!.waitForFunction(
|
||||
({ eventId, aId }: { eventId: string; aId: string }) =>
|
||||
(window as any).__testData.isParticipating(eventId, aId),
|
||||
{ eventId: out.eventId, aId: out.aId },
|
||||
{ timeout: 45000 },
|
||||
).then(() => true).catch(() => false);
|
||||
expect(aParticipates, 'A must be participating in its own event before B arrives').to.be.true;
|
||||
(this as any).isoEventId = out.eventId;
|
||||
(this as any).isoEventTitle = title;
|
||||
(this as any).isoAId = out.aId;
|
||||
|
||||
@@ -59,45 +59,58 @@ When('une page fraîche pour la MÊME identité A recharge sur le même wallet',
|
||||
// flake. This mirrors how the app's own steps already poll participation.
|
||||
Then('l\'événement {string} est sur l\'accueil de la page fraîche A', { timeout: 60000 }, async function (this: FestipodWorld, title: string) {
|
||||
const freshFrame = (this as any).recoFreshFrame;
|
||||
const onHome = await freshFrame.evaluate(async (title: string) => {
|
||||
const td = (window as any).__testData;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
if (td.homeEventTitles().includes(title)) return true;
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
return td.homeEventTitles().includes(title);
|
||||
}, title);
|
||||
// OBSERVE the REACTIVE home set (homeEventTitles reads AD().getUserEvents fed by
|
||||
// the subscription push — NOT a broker re-read). waitForFunction re-evaluates the
|
||||
// reactive getter until the cold-start union read converges; no broker polling.
|
||||
const onHome = await freshFrame.waitForFunction(
|
||||
(title: string) => (window as any).__testData.homeEventTitles().includes(title),
|
||||
title,
|
||||
{ timeout: 30000 },
|
||||
).then(() => true).catch(() => false);
|
||||
expect(onHome, `"${title}" MUST appear on the fresh A page's home (getUserEvents(A) after reconnect)`).to.be.true;
|
||||
});
|
||||
|
||||
Then('la page fraîche A est participante de l\'événement {string}', { timeout: 60000 }, async function (this: FestipodWorld, title: string) {
|
||||
const freshFrame = (this as any).recoFreshFrame;
|
||||
const eventId = (this as any).isoEventId;
|
||||
const isPart = await freshFrame.evaluate(async ({ eventId }: { eventId: string }) => {
|
||||
const td = (window as any).__testData;
|
||||
const aId = await td.ensureCurrentUser();
|
||||
for (let i = 0; i < 30; i++) {
|
||||
if (td.isParticipating(eventId, aId)) return true;
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
return td.isParticipating(eventId, aId);
|
||||
}, { eventId });
|
||||
await freshFrame.evaluate(async () => { await (window as any).__testData.ensureCurrentUser(); });
|
||||
// OBSERVE the REACTIVE participation set (isParticipating reads AD() fed by the
|
||||
// subscription push — NOT a broker re-read). waitForFunction re-evaluates the
|
||||
// reactive getter until the cold-start union read converges; no broker polling.
|
||||
const isPart = await freshFrame.waitForFunction(
|
||||
(eventId: string) => {
|
||||
const td = (window as any).__testData;
|
||||
const aId = td.currentUserId;
|
||||
return !!aId && td.isParticipating(eventId, aId);
|
||||
},
|
||||
eventId,
|
||||
{ timeout: 30000 },
|
||||
).then(() => true).catch(() => false);
|
||||
expect(isPart, `The fresh A page MUST read its own participation in "${title}" after reconnect`).to.be.true;
|
||||
});
|
||||
|
||||
Then('le compte autoritatif de participation de A à l\'événement {string} est {int}', { timeout: 60000 }, async function (this: FestipodWorld, _title: string, expected: number) {
|
||||
const freshFrame = (this as any).recoFreshFrame;
|
||||
const eventId = (this as any).isoEventId;
|
||||
const count = await freshFrame.evaluate(async ({ eventId, expected }: { eventId: string; expected: number }) => {
|
||||
await freshFrame.evaluate(async () => { await (window as any).__testData.ensureCurrentUser(); });
|
||||
// First wait for the REACTIVE state to reflect the expectation (the sign the
|
||||
// cold-start sync has passed — observes AD().isParticipating fed by the push, NOT
|
||||
// a broker re-read). THEN do ONE authoritative broker read.
|
||||
await freshFrame.waitForFunction(
|
||||
({ eventId, expected }: { eventId: string; expected: number }) => {
|
||||
const td = (window as any).__testData;
|
||||
const aId = td.currentUserId;
|
||||
if (!aId) return false;
|
||||
const isPart = td.isParticipating(eventId, aId);
|
||||
return expected > 0 ? isPart : !isPart;
|
||||
},
|
||||
{ eventId, expected },
|
||||
{ timeout: 30000 },
|
||||
).catch(() => { /* fall through to the single authoritative read */ });
|
||||
const count = await freshFrame.evaluate(async ({ eventId }: { eventId: string }) => {
|
||||
const td = (window as any).__testData;
|
||||
const aId = await td.ensureCurrentUser();
|
||||
let last = -1;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
last = await td.authParticipationCount(eventId, aId);
|
||||
if (last === expected) return last;
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
return last;
|
||||
}, { eventId, expected });
|
||||
return td.authParticipationCount(eventId, aId);
|
||||
}, { eventId });
|
||||
expect(count, `Authoritative broker count of A's participation must be ${expected} after reconnect`).to.equal(expected);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user