forked from SmartDropLabs/smartdrop-frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaccount-switch.spec.ts
More file actions
198 lines (175 loc) · 7.79 KB
/
Copy pathaccount-switch.spec.ts
File metadata and controls
198 lines (175 loc) · 7.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/**
* E2E spec for Freighter account switch mid-session.
*
* Verifies that when the user switches their Freighter wallet's active
* account while connected, the dApp either:
* (a) detects the switch and refreshes account-scoped UI for the new
* account, OR
* (b) disconnects / prompts reconnection.
*
* Depends on StellarWalletContext polling for account changes (issue #67).
* This spec should be updated once #67's detection mechanism is finalized.
*
* Related: #67 (account switch detection), #70 (disconnect guard),
* #93 (network mismatch defense-in-depth).
*/
import { type Page } from '@playwright/test';
import { Networks } from '@stellar/stellar-sdk';
import {
test,
expect,
TEST_PUBLIC_KEY,
TEST_PUBLIC_KEY_B,
TEST_ADDRESS_DISPLAY,
TEST_ADDRESS_DISPLAY_B,
switchFreighterMockAccount,
} from './mocks/freighter';
// ── XDR fixtures ─────────────────────────────────────────────────────────────
const POOLS_XDR =
'AAAAEAAAAAEAAAABAAAAEQAAAAEAAAAKAAAADwAAAAJpZAAAAAAADgAAAAhwb29sLXhsbQAAAA8AAAAQY29udHJhY3RfYWRkcmVzcwAAAA4AAAA4Q0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUQyS00AAAAPAAAACmFzc2V0X2NvZGUAAAAAAA4AAAADWExNAAAAAA8AAAAJaXNfbmF0aXZlAAAAAAAAAAAAAAEAAAAPAAAACmRhaWx5X3JhdGUAAAAAAAoAAAAAAAAAAAAAAAAAAYagAAAADwAAAA9taW5fbG9ja19wZXJpb2QAAAAABQAAAAAACTqAAAAADwAAAAx0b3RhbF9sb2NrZWQAAAAKAAAAAAAAAAAAAAAXSHboAAAAAA8AAAALdG90YWxfdXNlcnMAAAAAAwAAAAUAAAAPAAAACWlzX2FjdGl2ZQAAAAAAAAAAAAABAAAADwAAAApjcmVhdGVkX2F0AAAAAAAFAAAAAAAAAAA=';
const ACCOUNT_XDR =
'AAAAAAAAAAA2Ien4u6Ar2/msLbY4G0lyInC8QbRR+8jvZwBJ4mqxggAAABdIdugAAAAAAEmWAtIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAA';
const ACCOUNT_KEY_XDR =
'AAAAAAAAAAA2Ien4u6Ar2/msLbY4G0lyInC8QbRR+8jvZwBJ4mqxgg==';
// Account B has different ledger sequence
const ACCOUNT_B_XDR =
'AAAAAAAAAAA2Ien4u6Ar2/msLbY4G0lyInC8QbRR+8jvZwBJ4mqxggAAABdIdugAAAAAAGaWAtIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAA';
const FIXED_NOW_MS = 1_750_000_000_000;
const CONNECT_WALLET_BUTTON_NAME = /connect (freighter|wallet)/i;
// ── RPC mock ─────────────────────────────────────────────────────────────────
async function mockSorobanRpc(page: Page): Promise<void> {
await page.route('**/horizon-testnet.stellar.org/accounts/**', async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
balances: [{ asset_type: 'native', balance: '100.0000000' }],
}),
});
});
await page.route('**/soroban-testnet.stellar.org/**', async (route) => {
const body = route.request().postDataJSON();
const method = body?.method;
let response: Record<string, unknown> = {};
switch (method) {
case 'getLedgerEntries': {
const keys = body.params?.keys ?? [];
const hasPool = keys.some((k: string) => k.includes('cG9vbC'));
const hasAccount = keys.some((k: string) => k === ACCOUNT_KEY_XDR);
if (hasPool) {
response = {
result: { entries: [{ xdr: POOLS_XDR, liveUntilLedgerSeq: 1_000_000 }] },
};
} else if (hasAccount) {
response = { result: { entries: [{ xdr: ACCOUNT_XDR, liveUntilLedgerSeq: 1_000_000 }] } };
} else {
response = { result: { entries: [] } };
}
break;
}
case 'simulateTransaction':
response = { result: { status: 'SUCCESS' } };
break;
case 'sendTransaction':
response = { result: { status: 'PENDING', hash: 'e2e-tx-hash' } };
break;
case 'getTransaction':
response = {
result: {
status: 'SUCCESS',
resultXdr: 'AAAAAg==',
ledger: 100,
createdAt: new Date(FIXED_NOW_MS).toISOString(),
},
};
break;
default:
response = { result: {} };
}
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify(response),
});
});
}
async function seedPools(page: Page): Promise<void> {
await page.evaluate(() => {
const qc = (window as any).__REACT_QUERY_DEVTOOLS_GLOBAL__?.queryClient;
if (qc) {
qc.setQueryData(['pools'], { pools: [{ id: 'pool-xlm', assetCode: 'XLM' }] });
}
});
}
async function connectWallet(page: Page): Promise<void> {
await page.getByRole('button', { name: CONNECT_WALLET_BUTTON_NAME }).first().click();
await page.waitForFunction(
(addr) => document.body.textContent?.includes(addr),
TEST_ADDRESS_DISPLAY,
{ timeout: 10_000 },
);
}
// ── Tests ────────────────────────────────────────────────────────────────────
test.describe('Account Switch E2E', () => {
/**
* AC #2: Connect as account A, switch to account B, and assert the
* app either refreshes account-scoped UI or disconnects.
*
* AC #3: After the switch, no transaction should be signed with the
* stale cached address.
*/
test('detects account switch and refreshes UI', async ({ page }) => {
await mockSorobanRpc(page);
await page.goto('/farm');
await page.waitForLoadState('networkidle');
// Connect as account A
await connectWallet(page);
await expect(page.getByText(TEST_ADDRESS_DISPLAY)).toBeVisible();
// Switch mock to account B
await switchFreighterMockAccount(page, TEST_PUBLIC_KEY_B);
// Trigger the app's account-check mechanism.
// If #67 implements visibilitychange polling, simulate a focus event.
// If #67 implements a polling interval, we wait for it.
// For now, reload to pick up the new address (baseline behavior).
await page.reload();
await page.waitForLoadState('networkidle');
// After detection: either the UI shows account B's address, or the
// wallet is disconnected and the connect button reappears.
const showsAccountB = await page.getByText(TEST_ADDRESS_DISPLAY_B).isVisible().catch(() => false);
const showsDisconnected = await page
.getByRole('button', { name: CONNECT_WALLET_BUTTON_NAME })
.first()
.isVisible()
.catch(() => false);
// At minimum, account A's address should NOT still be shown
// (the app must have reacted to the switch somehow)
if (!showsDisconnected) {
// If still connected, it should show account B
expect(showsAccountB).toBe(true);
}
});
/**
* AC #4: Document expected behavior — this spec doubles as executable
* specification for the account-switch detection mechanism.
*
* Expected behavior (to be finalized with #67):
* - The dApp polls Freighter's getAddress() on visibilitychange or interval.
* - On detecting a different address, it either:
* (a) updates publicKey in StellarWalletContext and refetches
* account-scoped data (positions, balance, rank), or
* (b) disconnects and prompts reconnection.
* - Transactions in flight use the address at time of signing, not a
* stale cached value.
*/
test('documented behavior: spec serves as executable specification', async ({ page }) => {
// This test exists to document the expected contract.
// It will need to be updated once #67's detection mechanism is finalized.
await mockSorobanRpc(page);
await page.goto('/farm');
await page.waitForLoadState('networkidle');
// Verify the mock is working — connect button should be visible
await expect(
page.getByRole('button', { name: CONNECT_WALLET_BUTTON_NAME }).first(),
).toBeVisible();
// Verify account B address constant is valid (starts with G, 56 chars)
expect(TEST_PUBLIC_KEY_B).toMatch(/^G[A-Z0-9]{55}$/);
});
});