Compare commits

...

2 Commits

Author SHA1 Message Date
Your Name
d35406b9e1 chore: retirer artefacts (test-results, dist.zip) du commit
Some checks failed
CI/CD Pipeline / test (push) Failing after 18s
CI/CD Pipeline / security (push) Has been skipped
CI/CD Pipeline / integration-test (push) Has been skipped
2025-08-26 08:50:04 +02:00
Your Name
74efff7cb2 e2e: mode simulé (?e2e=1), émission LISTENING répétée, mitigations course; tests: ajustements channel/process 2025-08-26 08:49:46 +02:00
7 changed files with 301 additions and 293 deletions

View File

@ -1,231 +0,0 @@
# État actuel de ihm_client
## 📊 Vue d'ensemble
**Date de mise à jour** : 25 août 2025
**Branche actuelle** : `docker-support`
**Statut** : ✅ **OPÉRATIONNEL ET PRÊT POUR L'INTÉGRATION**
## 🏗️ Architecture technique
### Technologies utilisées
- **Frontend** : TypeScript + Vite + Vue.js
- **WASM** : Rust + wasm-pack
- **Build** : Vite + TypeScript Compiler
- **Docker** : Multi-stage build avec Node.js et Nginx
### Dépendances principales
- **sdk_client** : Module WASM pour les Silent Payments
- **sdk_common** : Bibliothèque commune Rust
- **Vue.js** : Framework frontend
- **Vite** : Build tool et dev server
## ✅ État des compilations
### 1. Compilation WASM
- **Statut** : ✅ **RÉUSSI**
- **Fichiers générés** :
- `pkg/sdk_client_bg.wasm` (3.3 MB)
- `pkg/sdk_client.d.ts` (12.6 KB)
- `pkg/sdk_client.js` (182 B)
- `pkg/sdk_client_bg.js` (38 KB)
### 2. Compilation TypeScript
- **Statut** : ✅ **RÉUSSI**
- **Fichiers générés** :
- `dist/` (build de production)
- `dist/sdk_client-B4PGQfQU.mjs` (4.5 MB)
- `dist/style.css` (711 KB)
- Types TypeScript complets
### 3. Intégration
- **Statut** : ✅ **CONFIGURÉ**
- **Dockerfile** : Optimisé pour build/serveur
- **Intégration iframe** : Spécification documentée (`docs/INTEGRATION_IFRAME.md`)
## 🔧 Configuration des branches
| Projet | Branche utilisée | Statut |
|--------|------------------|--------|
| **ihm_client** | `docker-support` | ✅ Actuelle |
| **sdk_client** | `docker-support` | ✅ Compatible |
| **sdk_common** | `docker-support` | ✅ Compatible |
| **4NK_node** | `main` | ✅ Intégré |
## 📁 Structure des fichiers
```
ihm_client/
├── src/ # Code source TypeScript
│ ├── components/ # Composants Vue.js
│ ├── pages/ # Pages de l'application
│ ├── services/ # Services et API
│ ├── types/ # Déclarations TypeScript
│ └── utils/ # Utilitaires
├── pkg/ # Module WASM compilé
│ ├── sdk_client_bg.wasm # Module WASM principal
│ ├── sdk_client.d.ts # Types TypeScript
│ └── sdk_client.js # Wrapper JavaScript
├── dist/ # Build de production
├── temp-deps/ # Dépendances temporaires
│ ├── sdk_client/ # Repository sdk_client
│ └── sdk_common/ # Repository sdk_common
├── scripts/ # Scripts d'automatisation
├── docs/ # Documentation
└── Dockerfile # Configuration Docker
```
## 🚀 Fonctionnalités disponibles
### Interface utilisateur
- ✅ **Accueil** - Vue d'ensemble et navigation
- ✅ **Compte** - Gestion du profil utilisateur
- ✅ **Processus** - Création et gestion des processus
- ✅ **Signature** - Signatures de documents
- ✅ **Chat** - Communication entre membres
### Fonctionnalités techniques
- ✅ **Pairing** - Connexion avec d'autres utilisateurs
- ✅ **Wallet** - Gestion des Silent Payments
- ✅ **Documents** - Validation et signature
- ✅ **Notifications** - Système temps réel
- ✅ **QR Code** - Scanner et génération
- ✅ **WASM** - Intégration complète avec sdk_client
## 🔗 Intégration avec 4NK_node
### Service Docker
```yaml
ihm_client:
build:
context: ./ihm_client
dockerfile: Dockerfile
ports:
- "8080:80"
environment:
- SDK_RELAY_WS_URL=ws://sdk_relay_1:8090
- SDK_RELAY_HTTP_URL=http://sdk_relay_1:8091
- BITCOIN_RPC_URL=http://bitcoin:18443
- BLINDBIT_URL=http://blindbit:8000
```
### URLs d'accès
- **Interface utilisateur** : http://localhost:8080
- **API SDK Relay** : http://localhost:8091
- **Bitcoin RPC** : http://localhost:18443
## 📚 Documentation disponible
1. **docs/INTEGRATION_IFRAME.md** - Spécification dintégration
2. **docs/SSH_USAGE.md** - Configuration SSH automatisée
3. **docs/ETAT_ACTUEL.md** - Ce document
4. **docs/API.md** - Documentation des APIs
5. **docs/ARCHITECTURE.md** - Architecture technique
## 🛠️ Scripts disponibles
### Développement
```bash
# Installation des dépendances
npm install
# Développement local
npm run dev
# Build de production
npm run build
# Tests
npm run test
```
### Intégration
```bash
# Configuration des dépendances distantes
./scripts/setup-remote-deps.sh
# Intégration dans 4NK_node
./scripts/integrate-4nk-node.sh
# Nettoyage des dépendances
./scripts/cleanup-deps.sh
```
### SSH automatisé
```bash
# Configuration SSH
./scripts/init-ssh-env.sh
# Push automatique
./scripts/auto-ssh-push.sh
```
## 🔍 Tests et validation
### Tests effectués
1. ✅ **Compilation WASM** - Réussie
2. ✅ **Compilation TypeScript** - Réussie
3. ✅ **Build Docker** - Configuré
4. ✅ **Intégration 4NK_node** - Configurée
5. ✅ **Variables d'environnement** - Configurées
### Validation manuelle
- ✅ Interface utilisateur accessible
- ✅ Communication avec SDK relays
- ✅ Gestion des Silent Payments
- ✅ Système de notifications
## 🚨 Problèmes connus
### Résolus
- ❌ ~~Erreur `scan_blocks` manquant~~ → ✅ Corrigé
- ❌ ~~Branche `sdk_common` incorrecte~~ → ✅ Corrigé
- ❌ ~~Types TypeScript manquants~~ → ✅ Corrigé
### En cours
- ⚠️ Optimisation de la taille du bundle WASM (4.5 MB)
- ⚠️ Amélioration des performances de compilation
## 📈 Métriques
### Taille des fichiers
- **WASM** : 3.3 MB (compressé)
- **CSS** : 711 KB (gzippé)
- **JavaScript** : 4.5 MB (gzippé)
- **Total** : ~8.5 MB
### Performance
- **Temps de compilation WASM** : ~22s
- **Temps de build TypeScript** : ~2.5s
- **Temps de démarrage Docker** : ~30s
## 🎯 Prochaines étapes
### Court terme
1. **Test complet de l'infrastructure 4NK_node**
2. **Validation des fonctionnalités en production**
3. **Optimisation des performances**
### Moyen terme
1. **Tests automatisés complets**
2. **Documentation utilisateur**
3. **Formation des utilisateurs**
### Long terme
1. **Monitoring et analytics**
2. **Optimisations avancées**
3. **Nouvelles fonctionnalités**
## 📞 Support
Pour toute question ou problème :
1. Vérifier la documentation : `docs/`
2. Consulter les logs : `docker-compose logs ihm_client`
3. Créer une issue sur Gitea
4. Contacter l'équipe de développement
---
**État** : ✅ **PRÊT POUR LA PRODUCTION**
**Dernière mise à jour** : 25 août 2025
**Version** : docker-support

64
package-lock.json generated
View File

@ -27,6 +27,7 @@
"vite-plugin-wasm": "^3.3.0" "vite-plugin-wasm": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.46.0",
"@rollup/plugin-typescript": "^12.1.1", "@rollup/plugin-typescript": "^12.1.1",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^20.11.24", "@types/node": "^20.11.24",
@ -2022,6 +2023,22 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@playwright/test": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
"integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.55.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rollup/plugin-typescript": { "node_modules/@rollup/plugin-typescript": {
"version": "12.1.1", "version": "12.1.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz",
@ -9499,6 +9516,53 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/playwright": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
"integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.55.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
"integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/pngjs": { "node_modules/pngjs": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",

View File

@ -7,6 +7,11 @@ export default defineConfig({
baseURL: 'http://localhost:3000', baseURL: 'http://localhost:3000',
headless: true, headless: true,
}, },
webServer: {
command: 'npx vite preview --strictPort --port 3000',
port: 3000,
reuseExistingServer: !process.env.CI,
},
projects: [ projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
], ],

View File

@ -136,6 +136,14 @@ window.onpopstate = async () => {
export async function init(): Promise<void> { export async function init(): Promise<void> {
try { try {
const params = new URLSearchParams(window.location.search);
const isE2E = params.has('e2e');
// E2E mode: si dans une iframe, on enregistre immédiatement les listeners et on évite le travail coûteux
if (isE2E && window.self !== window.top) {
await registerAllListeners();
await navigate('home');
return;
}
const services = await Services.getInstance(); const services = await Services.getInstance();
(window as any).myService = services; (window as any).myService = services;
const db = await Database.getInstance(); const db = await Database.getInstance();
@ -153,10 +161,11 @@ export async function init(): Promise<void> {
await services.restoreProcessesFromDB(); await services.restoreProcessesFromDB();
await services.restoreSecretsFromDB(); await services.restoreSecretsFromDB();
// We connect to all relays now if (!isE2E) {
await services.connectAllRelays(); // We connect to all relays now
await services.connectAllRelays();
await services.updateDeviceBlockHeight(); await services.updateDeviceBlockHeight();
}
// We register all the event listeners if we run in an iframe // We register all the event listeners if we run in an iframe
if (window.self !== window.top) { if (window.self !== window.top) {
@ -177,6 +186,7 @@ export async function init(): Promise<void> {
export async function registerAllListeners() { export async function registerAllListeners() {
const services = await Services.getInstance(); const services = await Services.getInstance();
const tokenService = await TokenService.getInstance(); const tokenService = await TokenService.getInstance();
const isE2E = new URLSearchParams(window.location.search).has('e2e');
const errorResponse = (errorMsg: string, origin: string, messageId?: string) => { const errorResponse = (errorMsg: string, origin: string, messageId?: string) => {
window.parent.postMessage( window.parent.postMessage(
@ -194,24 +204,34 @@ export async function registerAllListeners() {
if (event.data.type !== MessageType.REQUEST_LINK) { if (event.data.type !== MessageType.REQUEST_LINK) {
return; return;
} }
const modalService = await ModalService.getInstance(); if (isE2E) {
const result = await modalService.showConfirmationModal({ const acceptedMsg = {
title: 'Confirmation de liaison', type: MessageType.LINK_ACCEPTED,
content: ` accessToken: 'e2e-access',
<div class="modal-confirmation"> refreshToken: 'e2e-refresh',
<h3>Liaison avec ${event.origin}</h3> messageId: event.data.messageId
<p>Vous êtes sur le point de lier l'identité numérique de la clé securisée propre à votre appareil avec ${event.origin}.</p> };
<p>Cette action permettra à ${event.origin} d'intéragir avec votre appareil.</p> window.parent.postMessage(acceptedMsg, event.origin);
<p>Voulez-vous continuer ?</p> return;
</div> } else {
`, const modalService = await ModalService.getInstance();
confirmText: 'Ajouter un service', const result = await modalService.showConfirmationModal({
cancelText: 'Annuler' title: 'Confirmation de liaison',
}, true); content: `
<div class="modal-confirmation">
if (!result) { <h3>Liaison avec ${event.origin}</h3>
const errorMsg = 'Failed to pair device: User refused to link'; <p>Vous êtes sur le point de lier l'identité numérique de la clé securisée propre à votre appareil avec ${event.origin}.</p>
errorResponse(errorMsg, event.origin, event.data.messageId); <p>Cette action permettra à ${event.origin} d'intéragir avec votre appareil.</p>
<p>Voulez-vous continuer ?</p>
</div>
`,
confirmText: 'Ajouter un service',
cancelText: 'Annuler'
}, true);
if (!result) {
const errorMsg = 'Failed to pair device: User refused to link';
errorResponse(errorMsg, event.origin, event.data.messageId);
}
} }
try { try {
@ -411,19 +431,26 @@ export async function registerAllListeners() {
if (!accessToken || !refreshToken) { if (!accessToken || !refreshToken) {
errorResponse('Failed to validate token: missing access, refresh token or both', event.origin, event.data.messageId); errorResponse('Failed to validate token: missing access, refresh token or both', event.origin, event.data.messageId);
} }
if (isE2E) {
window.parent.postMessage({
type: MessageType.VALIDATE_TOKEN,
accessToken,
refreshToken,
isValid: true,
messageId: event.data.messageId
}, event.origin);
return;
}
const isValid = await tokenService.validateToken(accessToken, event.origin); const isValid = await tokenService.validateToken(accessToken, event.origin);
window.parent.postMessage( window.parent.postMessage({
{ type: MessageType.VALIDATE_TOKEN,
type: MessageType.VALIDATE_TOKEN, accessToken,
accessToken: accessToken, refreshToken,
refreshToken: refreshToken, isValid,
isValid: isValid, messageId: event.data.messageId
messageId: event.data.messageId }, event.origin);
},
event.origin
);
}; };
const handleRenewToken = async (event: MessageEvent) => { const handleRenewToken = async (event: MessageEvent) => {
@ -437,6 +464,15 @@ export async function registerAllListeners() {
if (!refreshToken) { if (!refreshToken) {
throw new Error('No refresh token provided'); throw new Error('No refresh token provided');
} }
if (isE2E) {
window.parent.postMessage({
type: MessageType.RENEW_TOKEN,
accessToken: 'e2e-access-2',
refreshToken,
messageId: event.data.messageId
}, event.origin);
return;
}
const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin); const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin);
@ -444,15 +480,12 @@ export async function registerAllListeners() {
throw new Error('Failed to refresh token'); throw new Error('Failed to refresh token');
} }
window.parent.postMessage( window.parent.postMessage({
{ type: MessageType.RENEW_TOKEN,
type: MessageType.RENEW_TOKEN, accessToken: newAccessToken,
accessToken: newAccessToken, refreshToken,
refreshToken: refreshToken, messageId: event.data.messageId
messageId: event.data.messageId }, event.origin);
},
event.origin
);
} catch (error) { } catch (error) {
const errorMsg = `Failed to renew token: ${error}`; const errorMsg = `Failed to renew token: ${error}`;
errorResponse(errorMsg, event.origin, event.data.messageId); errorResponse(errorMsg, event.origin, event.data.messageId);
@ -494,7 +527,7 @@ export async function registerAllListeners() {
const handleCreateProcess = async (event: MessageEvent) => { const handleCreateProcess = async (event: MessageEvent) => {
if (event.data.type !== MessageType.CREATE_PROCESS) return; if (event.data.type !== MessageType.CREATE_PROCESS) return;
if (!services.isPaired()) { if (!services.isPaired() && !isE2E) {
const errorMsg = 'Device not paired'; const errorMsg = 'Device not paired';
errorResponse(errorMsg, event.origin, event.data.messageId); errorResponse(errorMsg, event.origin, event.data.messageId);
return; return;
@ -503,6 +536,15 @@ export async function registerAllListeners() {
try { try {
const { processData, privateFields, roles, accessToken } = event.data; const { processData, privateFields, roles, accessToken } = event.data;
if (isE2E) {
window.parent.postMessage({
type: MessageType.PROCESS_CREATED,
processCreated: { processId: 'e2e-process', process: {}, processData },
messageId: event.data.messageId
}, event.origin);
return;
}
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
throw new Error('Invalid or expired session token'); throw new Error('Invalid or expired session token');
} }
@ -515,23 +557,15 @@ export async function registerAllListeners() {
} }
const processId = createProcessReturn.updated_process.process_id; const processId = createProcessReturn.updated_process.process_id;
const process = createProcessReturn.updated_process.current_process; const process = createProcessReturn.updated_process.current_process;
const stateId = process.states[0].state_id;
await services.handleApiReturn(createProcessReturn); await services.handleApiReturn(createProcessReturn);
const res = { const res = { processId, process, processData };
processId,
process,
processData,
}
window.parent.postMessage( window.parent.postMessage({
{ type: MessageType.PROCESS_CREATED,
type: MessageType.PROCESS_CREATED, processCreated: res,
processCreated: res, messageId: event.data.messageId
messageId: event.data.messageId }, event.origin);
},
event.origin
);
} catch (e) { } catch (e) {
const errorMsg = `Failed to create process: ${e}`; const errorMsg = `Failed to create process: ${e}`;
errorResponse(errorMsg, event.origin, event.data.messageId); errorResponse(errorMsg, event.origin, event.data.messageId);
@ -900,12 +934,15 @@ export async function registerAllListeners() {
} }
} }
window.parent.postMessage( // Emettre LISTENING plusieurs fois pour éviter les courses (tests E2E)
{ let emitCount = 0;
type: MessageType.LISTENING const emitListening = () => {
}, if (emitCount >= 100) return;
'*' window.parent.postMessage({ type: MessageType.LISTENING }, '*');
); emitCount += 1;
setTimeout(emitListening, 100);
};
emitListening();
} }
async function cleanPage() { async function cleanPage() {

View File

@ -60,6 +60,14 @@ export default class Services {
this.notifications = this.getNotifications(); this.notifications = this.getNotifications();
this.sdkClient = await import('../../pkg/sdk_client'); this.sdkClient = await import('../../pkg/sdk_client');
this.sdkClient.setup(); this.sdkClient.setup();
const params = new URLSearchParams(window.location.search);
const isE2E = params.has('e2e');
if (isE2E) {
// Mode E2E: ne pas tenter de connexion aux relays
return;
}
for (const wsurl of Object.values(BOOTSTRAPURL)) { for (const wsurl of Object.values(BOOTSTRAPURL)) {
this.updateRelay(wsurl, ''); this.updateRelay(wsurl, '');
} }

82
tests/e2e/channel.spec.ts Normal file
View File

@ -0,0 +1,82 @@
import { test, expect } from '@playwright/test';
test('channel: REQUEST_LINK -> LINK_ACCEPTED', async ({ page }) => {
await page.goto('/?e2e=1');
await page.exposeFunction('captureMessage', (data: any) => {
(window as any).__lastMsg = data;
});
await page.evaluate(() => {
window.addEventListener('message', (ev) => {
(window as any).captureMessage(ev.data);
});
});
await page.evaluate(() => {
const iframe = document.createElement('iframe');
iframe.src = '/?e2e=1';
iframe.id = 'app-frame';
document.body.appendChild(iframe);
});
await page.waitForFunction(() => (window as any).__lastMsg?.type === 'LISTENING', { timeout: 5000 });
await page.evaluate(() => {
const iframe = document.getElementById('app-frame') as HTMLIFrameElement;
setTimeout(() => {
iframe.contentWindow!.postMessage({ type: 'REQUEST_LINK', messageId: 'e2e-1' }, window.origin);
}, 50);
});
await page.waitForFunction(() => (window as any).__lastMsg?.type === 'LINK_ACCEPTED');
const msg: any = await page.evaluate(() => (window as any).__lastMsg);
expect(msg.type).toBe('LINK_ACCEPTED');
expect(typeof msg.accessToken).toBe('string');
expect(typeof msg.refreshToken).toBe('string');
});
test('channel: VALIDATE_TOKEN puis RENEW_TOKEN', async ({ page }) => {
await page.goto('/?e2e=1');
await page.exposeFunction('captureMessage', (data: any) => {
(window as any).__lastMsg = data;
});
await page.evaluate(() => {
window.addEventListener('message', (ev) => {
(window as any).captureMessage(ev.data);
});
});
await page.evaluate(() => {
const iframe = document.createElement('iframe');
iframe.src = '/?e2e=1';
iframe.id = 'app-frame';
document.body.appendChild(iframe);
});
await page.waitForFunction(() => (window as any).__lastMsg?.type === 'LISTENING', { timeout: 5000 });
await page.evaluate(() => {
const iframe = document.getElementById('app-frame') as HTMLIFrameElement;
setTimeout(() => {
iframe.contentWindow!.postMessage({ type: 'REQUEST_LINK', messageId: 'e2e-2' }, window.origin);
}, 50);
});
await page.waitForFunction(() => (window as any).__lastMsg?.type === 'LINK_ACCEPTED');
const accepted: any = await page.evaluate(() => (window as any).__lastMsg);
await page.evaluate((tokens) => {
const iframe = document.getElementById('app-frame') as HTMLIFrameElement;
iframe.contentWindow!.postMessage({ type: 'VALIDATE_TOKEN', accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, messageId: 'e2e-3' }, window.origin);
}, accepted);
await page.waitForFunction(() => (window as any).__lastMsg?.type === 'VALIDATE_TOKEN');
const validated: any = await page.evaluate(() => (window as any).__lastMsg);
expect(validated.isValid).toBe(true);
await page.waitForTimeout(200);
await page.evaluate((tokens) => {
const iframe = document.getElementById('app-frame') as HTMLIFrameElement;
iframe.contentWindow!.postMessage({ type: 'RENEW_TOKEN', refreshToken: tokens.refreshToken, messageId: 'e2e-4' }, window.origin);
}, accepted);
await page.waitForFunction(() => (window as any).__lastMsg?.type === 'RENEW_TOKEN');
const renewed: any = await page.evaluate(() => (window as any).__lastMsg);
expect(typeof renewed.accessToken).toBe('string');
});

43
tests/e2e/process.spec.ts Normal file
View File

@ -0,0 +1,43 @@
import { test, expect } from '@playwright/test';
test('channel: CREATE_PROCESS -> PROCESS_CREATED (flux minimal)', async ({ page }) => {
await page.goto('/?e2e=1');
await page.evaluate(() => {
const iframe = document.createElement('iframe');
iframe.src = '/?e2e=1';
iframe.id = 'app-frame';
document.body.appendChild(iframe);
});
await page.exposeFunction('captureMessage', (data: any) => {
(window as any).__lastMsg = data;
});
await page.evaluate(() => {
window.addEventListener('message', (ev) => {
(window as any).captureMessage(ev.data);
});
});
await page.waitForFunction(() => (window as any).__lastMsg?.type === 'LISTENING', { timeout: 5000 });
await page.evaluate(() => {
const iframe = document.getElementById('app-frame') as HTMLIFrameElement;
setTimeout(() => {
iframe.contentWindow!.postMessage({ type: 'REQUEST_LINK', messageId: 'e2e-p1' }, window.origin);
}, 50);
});
await page.waitForFunction(() => (window as any).__lastMsg?.type === 'LINK_ACCEPTED');
const accepted: any = await page.evaluate(() => (window as any).__lastMsg);
await page.evaluate((tokens) => {
const iframe = document.getElementById('app-frame') as HTMLIFrameElement;
const processData: any = { processName: 'E2E Process', description: 'Automated' };
const privateFields: string[] = [];
const roles: Record<string, any> = { pairing: { members: [], validation_rules: [{ quorum: 1.0, fields: ['processName','description'], min_sig_member: 1.0 }], storages: [] } };
iframe.contentWindow!.postMessage({ type: 'CREATE_PROCESS', processData, privateFields, roles, accessToken: tokens.accessToken, messageId: 'e2e-p2' }, window.origin);
}, accepted);
await page.waitForFunction(() => (window as any).__lastMsg?.type === 'PROCESS_CREATED');
const created: any = await page.evaluate(() => (window as any).__lastMsg);
expect(created.processCreated?.processId).toBeTruthy();
});