Skip to content

Commit 965415c

Browse files
authored
Merge pull request #118 from walkframe/feature/async-function
feat: support async function
2 parents 4466c1e + b6f2bef commit 965415c

71 files changed

Lines changed: 3386 additions & 956 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

e2e/async.spec.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Async Formula', () => {
4+
test('should render async formula result after delay', async ({ page }) => {
5+
await page.goto('http://localhost:5233/iframe.html?id=formula-asyncchain--async-chain&viewMode=story');
6+
7+
const sheet = page.locator('[data-sheet-name="AsyncChain"]');
8+
const a1 = sheet.locator("[data-address='A1']");
9+
const a2 = sheet.locator("[data-address='A2']");
10+
const a3 = sheet.locator("[data-address='A3']");
11+
const a4 = sheet.locator("[data-address='A4']");
12+
const a5 = sheet.locator("[data-address='A5']");
13+
14+
// Before waiting, cells should be empty or show pending state
15+
const a1InitialContent = await a1.locator('.gs-cell-rendered').textContent();
16+
expect(a1InitialContent).toBe('');
17+
18+
const a4InitialContent = await a4.locator('.gs-cell-rendered').textContent();
19+
expect(a4InitialContent).toBe('');
20+
21+
// Wait for async results to resolve
22+
// A1 takes 1s, A2 depends on A1 (1s), A3 on A2 (1s), A4 on A3 (1s) = up to 4s
23+
// Adding buffer for render time
24+
await page.waitForTimeout(5000);
25+
26+
// A1: SUM_DELAY(10, 20) = 30
27+
const a1Content = await a1.locator('.gs-cell-rendered').textContent();
28+
expect(a1Content).toBe('30');
29+
30+
// A2: SUM_DELAY(A1, 100) = SUM_DELAY(30, 100) = 130
31+
const a2Content = await a2.locator('.gs-cell-rendered').textContent();
32+
expect(a2Content).toBe('130');
33+
34+
// A3: SUM_DELAY(A2, 200) = SUM_DELAY(130, 200) = 330
35+
const a3Content = await a3.locator('.gs-cell-rendered').textContent();
36+
expect(a3Content).toBe('330');
37+
38+
// A4: SUM_DELAY(A3, A1) = SUM_DELAY(330, 30) = 360
39+
const a4Content = await a4.locator('.gs-cell-rendered').textContent();
40+
expect(a4Content).toBe('360');
41+
42+
// A5: SUM(A1:A4) = 30 + 130 + 330 + 360 = 850
43+
const a5Content = await a5.locator('.gs-cell-rendered').textContent();
44+
expect(a5Content).toBe('850');
45+
});
46+
47+
test('should cache async formula result within TTL', async ({ page }) => {
48+
await page.goto('http://localhost:5233/iframe.html?id=formula-asyncchain--async-chain&viewMode=story');
49+
50+
const sheet = page.locator('[data-sheet-name="AsyncChain"]');
51+
const a1 = sheet.locator("[data-address='A1']");
52+
const b1 = sheet.locator("[data-address='B1']");
53+
54+
// Initially, A1 should be empty
55+
const a1InitialContent = await a1.locator('.gs-cell-rendered').textContent();
56+
expect(a1InitialContent).toBe('');
57+
58+
// Wait for first async computation (A1 takes 1 second + render time)
59+
await page.waitForTimeout(1500);
60+
61+
// A1 should have computed result
62+
let a1Content = await a1.locator('.gs-cell-rendered').textContent();
63+
expect(a1Content).toBe('30');
64+
65+
// Click on another cell to trigger re-render without changing A1
66+
await b1.click();
67+
68+
// A1 should still show the cached result (no additional wait needed)
69+
a1Content = await a1.locator('.gs-cell-rendered').textContent();
70+
expect(a1Content).toBe('30');
71+
});
72+
73+
test('should invalidate async cache when inputs change', async ({ page }) => {
74+
await page.goto('http://localhost:5233/iframe.html?id=formula-asyncchain--async-chain&viewMode=story');
75+
76+
const sheet = page.locator('[data-sheet-name="AsyncChain"]');
77+
const a1 = sheet.locator("[data-address='A1']");
78+
const a2 = sheet.locator("[data-address='A2']");
79+
80+
// Wait for first async computation
81+
// A1 takes 1s, then A2 depends on A1 (another 1s) = 2s + buffer
82+
await page.waitForTimeout(3000);
83+
84+
// A2 should depend on A1, initial value: SUM_DELAY(30, 100) = 130
85+
let a2Content = await a2.locator('.gs-cell-rendered').textContent();
86+
expect(a2Content).toBe('130');
87+
88+
// Change A1 value by double-clicking to enter edit mode
89+
await a1.click();
90+
await page.keyboard.type('=SUM_DELAY(40, 50)');
91+
await page.keyboard.press('Enter');
92+
93+
// Wait for re-computation: A1 takes 1s, then A2 depends on new A1 (another 1s) = 2s + buffer
94+
await page.waitForTimeout(3000);
95+
96+
// A1 should now be 90 (40 + 50)
97+
const a1NewContent = await a1.locator('.gs-cell-rendered').textContent();
98+
expect(a1NewContent).toBe('90');
99+
100+
// A2 should be updated to SUM_DELAY(90, 100) = 190
101+
const a2NewContent = await a2.locator('.gs-cell-rendered').textContent();
102+
expect(a2NewContent).toBe('190');
103+
});
104+
105+
test('should propagate pending through async dependency chain', async ({ page }) => {
106+
await page.goto('http://localhost:5233/iframe.html?id=formula-asyncchain--async-chain&viewMode=story');
107+
108+
const sheet = page.locator('[data-sheet-name="AsyncChain"]');
109+
const a1 = sheet.locator("[data-address='A1']");
110+
const a4 = sheet.locator("[data-address='A4']");
111+
112+
// Before waiting, cells should be empty
113+
const a1InitialContent = await a1.locator('.gs-cell-rendered').textContent();
114+
expect(a1InitialContent).toBe('');
115+
116+
const a4InitialContent = await a4.locator('.gs-cell-rendered').textContent();
117+
expect(a4InitialContent).toBe('');
118+
119+
// Wait for async dependency chain to resolve
120+
// A1 (1s) → A2 (1s) → A3 (1s) → A4 (1s) = 4s + buffer
121+
await page.waitForTimeout(5000);
122+
123+
const a1Content = await a1.locator('.gs-cell-rendered').textContent();
124+
expect(a1Content).toBe('30');
125+
126+
const a4Content = await a4.locator('.gs-cell-rendered').textContent();
127+
expect(a4Content).toBe('360');
128+
});
129+
130+
test('should display async error code #ASYNC! when async function throws', async ({ page }) => {
131+
await page.goto('http://localhost:5233/iframe.html?id=formula-asyncchain--async-chain&viewMode=story');
132+
133+
const sheet = page.locator('[data-sheet-name="AsyncChain"]');
134+
const a6 = sheet.locator("[data-address='A6']");
135+
136+
// A6 contains =SUM_DELAY() with no arguments, which should throw an error
137+
const a6Rendered = a6.locator('.gs-cell-rendered');
138+
const a6Content = await a6Rendered.textContent();
139+
140+
// Verify that the error code is displayed
141+
expect(a6Content?.trim()).toBe('#ASYNC!');
142+
});
143+
});

e2e/event.spec.ts

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -407,121 +407,121 @@ test.describe('OnEdit Event Tests', () => {
407407
});
408408

409409
test('should display edit data when writing to cells', async ({ page }) => {
410-
// Sheet1のA2セル(Apple)をクリック
410+
// Click cell A2 (Apple) in Sheet1
411411
await page.click('[data-sheet-name="Sheet1"] >> text=Apple');
412412

413-
// セルを編集モードにする(ダブルクリック)
413+
// Enter edit mode (double-click)
414414
await page.dblclick('[data-sheet-name="Sheet1"] .gs-cell >> text=Apple');
415415

416-
// 値を変更
416+
// Change the value
417417
await page.fill('[data-sheet-name="Sheet1"] input, [data-sheet-name="Sheet1"] textarea', 'Apple Updated');
418418
await page.keyboard.press('Enter');
419419

420-
// Edit Historyに変更が反映されることを確認
420+
// Verify the change is reflected in Edit History
421421
await page.waitForFunction(() => {
422422
const historyElements = document.querySelectorAll('[data-testid="history-item"]');
423423
return historyElements.length > 0;
424424
});
425425

426-
// 最新の履歴データを確認
426+
// Check the latest history data
427427
const latestDataText = await page.locator('[data-testid="history-data"]').first().inputValue();
428428
const editData = JSON.parse(latestDataText);
429429

430-
// A2セルの値が更新されていることを確認
430+
// Verify the value of cell A2 has been updated
431431
expect(editData).toHaveProperty('A2');
432-
expect(editData.A2).toBe('Apple Updated');
432+
expect(editData.A2.value).toBe('Apple Updated');
433433
});
434434

435435
test('should display edit data when moving cells', async ({ page }) => {
436-
// 簡単な編集操作でonEditが動作することを確認
436+
// Verify that onEdit fires for a simple edit operation
437437
await page.dblclick('[data-sheet-name="Sheet1"] .gs-cell >> text=Apple');
438438
await page.fill('[data-sheet-name="Sheet1"] input, [data-sheet-name="Sheet1"] textarea', 'Apple Moved');
439439
await page.keyboard.press('Enter');
440440

441-
// Edit Historyに編集の差分が反映されることを確認
441+
// Verify the edit diff is reflected in Edit History
442442
await page.waitForFunction(() => {
443443
const historyElements = document.querySelectorAll('[data-testid="history-item"]');
444444
return historyElements.length > 0;
445445
});
446446

447-
// 最新の履歴データを確認
447+
// Check the latest history data
448448
const latestDataText = await page.locator('[data-testid="history-data"]').first().inputValue();
449449
const editData = JSON.parse(latestDataText);
450450

451-
// 編集された値が存在することを確認
452-
const hasEditedData = Object.keys(editData).some((key) => editData[key] === 'Apple Moved');
451+
// Verify the edited value exists
452+
const hasEditedData = Object.keys(editData).some((key) => editData[key]?.value === 'Apple Moved');
453453
expect(hasEditedData).toBe(true);
454454
});
455455

456456
test('should display edit history for multiple operations', async ({ page }) => {
457-
// 複数のシートで複数の編集操作を実行
457+
// Perform multiple edit operations across multiple sheets
458458

459-
// 1. Sheet1のA2セルを編集
459+
// 1. Edit cell A2 in Sheet1
460460
await page.dblclick('[data-sheet-name="Sheet1"] .gs-cell >> text=Apple');
461461
await page.fill('[data-sheet-name="Sheet1"] input, [data-sheet-name="Sheet1"] textarea', 'Apple 1');
462462
await page.keyboard.press('Enter');
463463

464-
// 2. Sheet1のB2セルを編集
464+
// 2. Edit cell B2 in Sheet1
465465
await page.dblclick('[data-sheet-name="Sheet1"] .gs-cell >> text=100');
466466
await page.fill('[data-sheet-name="Sheet1"] input, [data-sheet-name="Sheet1"] textarea', '150');
467467
await page.keyboard.press('Enter');
468468

469-
// 3. Sheet2で編集
469+
// 3. Edit in Sheet2
470470
await page.click('[data-sheet-name="Sheet2"] .gs-cell >> text=John');
471471
await page.dblclick('[data-sheet-name="Sheet2"] .gs-cell >> text=John');
472472
await page.fill('[data-sheet-name="Sheet2"] input, [data-sheet-name="Sheet2"] textarea', 'John Updated');
473473
await page.keyboard.press('Enter');
474474

475-
// Edit Historyに複数のエントリが表示されることを確認
475+
// Verify multiple entries are shown in Edit History
476476
await page.waitForFunction(() => {
477477
const historyElements = document.querySelectorAll('[data-testid="history-item"]');
478478
return historyElements.length >= 3;
479479
});
480480

481-
// 履歴の内容を確認
481+
// Verify the history contents
482482
const historyElements = await page.locator('[data-testid="history-item"]').all();
483483
expect(historyElements.length).toBeGreaterThanOrEqual(3);
484484

485-
// 最新の履歴エントリを確認
485+
// Check the latest history entry
486486
const latestHistory = await historyElements[0].textContent();
487487
expect(latestHistory).toContain('Sheet:');
488488

489-
// 最新の履歴データを確認
489+
// Check the latest history data
490490
const latestDataText = await page.locator('[data-testid="history-data"]').first().inputValue();
491491
expect(latestDataText).toContain('John Updated');
492492
});
493493

494494
test('should show correct area information in edit data', async ({ page }) => {
495-
// Sheet1のA2セルを編集
495+
// Edit cell A2 in Sheet1
496496
await page.dblclick('[data-sheet-name="Sheet1"] .gs-cell >> text=Apple');
497497
await page.fill('[data-sheet-name="Sheet1"] input, [data-sheet-name="Sheet1"] textarea', 'Test Value');
498498
await page.keyboard.press('Enter');
499499

500-
// Edit Historyのエリア情報を確認
500+
// Check the area information in Edit History
501501
await page.waitForFunction(() => {
502502
const historyElements = document.querySelectorAll('[data-testid="history-item"]');
503503
return historyElements.length > 0;
504504
});
505505

506506
const historyText = await page.locator('[data-testid="history-item"]').first().textContent();
507507

508-
// エリア情報が正しく表示されていることを確認(A2セルなので top:2, left:1, bottom:2, right:1
508+
// Verify the area information is displayed correctly (cell A2: top:2, left:1, bottom:2, right:1)
509509
expect(historyText).toContain('Area:2,1 to 2,1');
510510
});
511511

512512
test('should handle edits on both sheets independently', async ({ page }) => {
513-
// Sheet1で編集
513+
// Edit in Sheet1
514514
await page.dblclick('[data-sheet-name="Sheet1"] .gs-cell >> text=Apple');
515515
await page.fill('[data-sheet-name="Sheet1"] input, [data-sheet-name="Sheet1"] textarea', 'Sheet1 Edit');
516516
await page.keyboard.press('Enter');
517517

518-
// Sheet2で編集
518+
// Edit in Sheet2
519519
await page.click('[data-sheet-name="Sheet2"] .gs-cell >> text=John');
520520
await page.dblclick('[data-sheet-name="Sheet2"] .gs-cell >> text=John');
521521
await page.fill('[data-sheet-name="Sheet2"] input, [data-sheet-name="Sheet2"] textarea', 'Sheet2 Edit');
522522
await page.keyboard.press('Enter');
523523

524-
// 両方の編集が履歴に記録されることを確認
524+
// Verify both edits are recorded in history
525525
await page.waitForFunction(() => {
526526
const historyElements = document.querySelectorAll('[data-testid="history-item"]');
527527
return historyElements.length >= 2;
@@ -530,7 +530,7 @@ test.describe('OnEdit Event Tests', () => {
530530
const historyElements = await page.locator('[data-testid="history-item"]').all();
531531
expect(historyElements.length).toBeGreaterThanOrEqual(2);
532532

533-
// 最新の編集がSheet2のものであることを確認
533+
// Verify the latest edit belongs to Sheet2
534534
const latestHistory = await historyElements[0].textContent();
535535
expect(latestHistory).toContain('Sheet2 Edit');
536536
});

e2e/history.spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ test('undo redo for cell writing', async ({ page }) => {
99
const originalA1Value = await a1.locator('.gs-cell-rendered').textContent();
1010

1111
await a1.click();
12-
const editor = page.locator('.gs-editor textarea');
13-
// Clear the editor first
14-
await editor.fill('');
1512
await page.keyboard.type('NewValue');
1613
await page.keyboard.press('Enter');
1714

@@ -90,8 +87,6 @@ test('multiple undo redo operations', async ({ page }) => {
9087

9188
// Multiple operations
9289
await a1.click();
93-
const editor = page.locator('.gs-editor textarea');
94-
await editor.fill('');
9590
await page.keyboard.type('First');
9691
await page.keyboard.press('Enter');
9792

0 commit comments

Comments
 (0)