| 207 | }; |
| 208 | |
| 209 | async waitUntilReady() { |
| 210 | const startRenderCount = this.renderCount; |
| 211 | if (!vi.isFakeTimers()) { |
| 212 | // Give Ink a chance to start its rendering loop |
| 213 | await new Promise((resolve) => setImmediate(resolve)); |
| 214 | } |
| 215 | await act(async () => { |
| 216 | if (vi.isFakeTimers()) { |
| 217 | await vi.advanceTimersByTimeAsync(50); |
| 218 | } else { |
| 219 | // Wait for at least one render to be called if we haven't rendered yet or since start of this call, |
| 220 | // but don't wait forever as some renders might be synchronous or skipped. |
| 221 | if (this.renderCount === startRenderCount) { |
| 222 | const renderPromise = new Promise((resolve) => |
| 223 | this.once('render', resolve), |
| 224 | ); |
| 225 | const timeoutPromise = new Promise((resolve) => |
| 226 | setTimeout(resolve, 1000), |
| 227 | ); |
| 228 | await Promise.race([renderPromise, timeoutPromise]); |
| 229 | } |
| 230 | } |
| 231 | }); |
| 232 | |
| 233 | let attempts = 0; |
| 234 | const maxAttempts = 50; |
| 235 | |
| 236 | let lastCurrent = ''; |
| 237 | let lastExpected = ''; |
| 238 | |
| 239 | while (attempts < maxAttempts) { |
| 240 | // Ensure all pending writes to the terminal are processed. |
| 241 | await this.queue.promise; |
| 242 | |
| 243 | const currentFrame = stripAnsi( |
| 244 | this.lastFrame({ allowEmpty: true }), |
| 245 | ).trim(); |
| 246 | const expectedFrame = this.normalizeFrame( |
| 247 | stripAnsi( |
| 248 | (this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? ''), |
| 249 | ), |
| 250 | ).trim(); |
| 251 | |
| 252 | lastCurrent = currentFrame; |
| 253 | lastExpected = expectedFrame; |
| 254 | |
| 255 | const isMatch = () => { |
| 256 | if (expectedFrame === '...') { |
| 257 | // '...' is our fallback when output isn't in metrics, meaning Ink rendered *something* |
| 258 | // but we don't know what it is. If terminal has content, we consider it a match. |
| 259 | // However, if the component rendered null, both would be empty, but our fallback |
| 260 | // made expectedFrame '...'. In that case, we can't easily know if it's ready, |
| 261 | // but we can assume if there are no pending writes, it's ready. |
| 262 | return currentFrame !== '' || this.pendingWrites === 0; |
| 263 | } |
| 264 | |
| 265 | // If Ink expects nothing (no new static content and no dynamic output), |
| 266 | // we consider it a match because the terminal buffer will just hold the historical static content. |