Skip to content

Commit ac617a6

Browse files
Merge pull request #24 from SteveLin100132/test/e2e-generator-chart
test: Add end-to-end tests for Notion Chart Generator with multi-select properties
2 parents 6150678 + 3dadb64 commit ac617a6

File tree

1 file changed

+329
-0
lines changed

1 file changed

+329
-0
lines changed
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
import { test } from "@playwright/test";
2+
import dotenv from "dotenv";
3+
4+
dotenv.config();
5+
6+
// Playwright 測試設定
7+
test.use({
8+
trace: "on",
9+
screenshot: "on",
10+
video: {
11+
mode: "on",
12+
size: { width: 1920, height: 1080 },
13+
},
14+
permissions: ["clipboard-read", "clipboard-write"],
15+
});
16+
17+
/**
18+
* Notion Chart Generator E2E 測試
19+
* 透過 Notion Multi Select 屬性篩選結果
20+
*/
21+
test("Notion Chart Generator - 透過 Notion Multi Select 屬性篩選結果", async ({
22+
page,
23+
}) => {
24+
// 設定測試超時時間,這裡設定為 120 秒,確保有足夠時間
25+
test.setTimeout(120000);
26+
27+
// 設定視窗大小
28+
await page.setViewportSize({ width: 1920, height: 1080 });
29+
30+
// 前往 Notion Chart Generator 網站
31+
await page.goto(process.env.E2E_TEST_HOST || "http://localhost:3000/");
32+
33+
// 注入滑鼠軌跡顯示腳本
34+
await page.evaluate(() => {
35+
const style = document.createElement("style");
36+
style.innerHTML = `
37+
.__pw-mouse-pointer {
38+
pointer-events: none;
39+
position: fixed;
40+
z-index: 9999;
41+
width: 24px;
42+
height: 24px;
43+
margin-left: -12px;
44+
margin-top: -12px;
45+
border-radius: 12px;
46+
background: rgba(255,0,0,0.5);
47+
box-shadow: 0 0 8px 2px rgba(255,0,0,0.3);
48+
transition: left 0.05s linear, top 0.05s linear;
49+
}
50+
`;
51+
document.head.appendChild(style);
52+
const pointer = document.createElement("div");
53+
pointer.className = "__pw-mouse-pointer";
54+
document.body.appendChild(pointer);
55+
document.addEventListener(
56+
"mousemove",
57+
(event) => {
58+
pointer.style.left = event.clientX + "px";
59+
pointer.style.top = event.clientY + "px";
60+
},
61+
true
62+
);
63+
});
64+
65+
// 輸入 Notion API 金鑰
66+
const notionApiKeyInput = page.getByRole("textbox", {
67+
name: "secret_xxx 或 ntn_xxx",
68+
});
69+
const notionApiKeyInputBoundingBox = await notionApiKeyInput.boundingBox();
70+
if (notionApiKeyInputBoundingBox) {
71+
await page.mouse.move(
72+
(notionApiKeyInputBoundingBox?.x ?? 0) +
73+
notionApiKeyInputBoundingBox.width / 2,
74+
(notionApiKeyInputBoundingBox?.y ?? 0) +
75+
notionApiKeyInputBoundingBox.height / 2,
76+
{ steps: 20 }
77+
);
78+
await page.waitForTimeout(250);
79+
}
80+
await notionApiKeyInput.fill(process.env.E2E_TEST_NOTION_API_KEY || "");
81+
await page.waitForTimeout(750);
82+
83+
// 點擊「載入資料庫」按鈕
84+
const loadDatabaseButton = page.getByRole("button", { name: "載入資料庫" });
85+
const loadDatabaseButtonBoundingBox = await loadDatabaseButton.boundingBox();
86+
if (loadDatabaseButtonBoundingBox) {
87+
await page.mouse.move(
88+
(loadDatabaseButtonBoundingBox?.x ?? 0) +
89+
loadDatabaseButtonBoundingBox.width / 2,
90+
(loadDatabaseButtonBoundingBox?.y ?? 0) +
91+
loadDatabaseButtonBoundingBox.height / 2,
92+
{ steps: 20 }
93+
);
94+
await page.waitForTimeout(250);
95+
}
96+
await loadDatabaseButton.click();
97+
await page.waitForTimeout(750);
98+
99+
// 等待資料庫載入完成
100+
const databaseListComboBox = page.getByRole("combobox");
101+
await databaseListComboBox.waitFor({ state: "visible" });
102+
const databaseListComboBoxBoundingBox =
103+
await databaseListComboBox.boundingBox();
104+
if (databaseListComboBoxBoundingBox) {
105+
await page.mouse.move(
106+
(databaseListComboBoxBoundingBox?.x ?? 0) +
107+
databaseListComboBoxBoundingBox.width / 2,
108+
(databaseListComboBoxBoundingBox?.y ?? 0) +
109+
databaseListComboBoxBoundingBox.height / 2,
110+
{ steps: 20 }
111+
);
112+
await page.waitForTimeout(250);
113+
}
114+
await databaseListComboBox.click();
115+
116+
// 選擇「新知資料庫」資料庫
117+
const knowledgeOption = page.getByRole("option", { name: "新知資料庫" });
118+
const knowledgeOptionBoundingBox = await knowledgeOption.boundingBox();
119+
if (knowledgeOptionBoundingBox) {
120+
await page.mouse.move(
121+
(knowledgeOptionBoundingBox?.x ?? 0) +
122+
knowledgeOptionBoundingBox.width / 2,
123+
(knowledgeOptionBoundingBox?.y ?? 0) +
124+
knowledgeOptionBoundingBox.height / 2,
125+
{ steps: 20 }
126+
);
127+
await page.waitForTimeout(250);
128+
}
129+
await knowledgeOption.click();
130+
await page.waitForTimeout(750);
131+
132+
// 選擇 X 軸屬性
133+
const xAxisCombobox = page
134+
.getByRole("combobox")
135+
.filter({ hasText: "選擇 X 軸屬性" });
136+
const xAxisComboboxBoundingBox = await xAxisCombobox.boundingBox();
137+
if (xAxisComboboxBoundingBox) {
138+
await page.mouse.move(
139+
(xAxisComboboxBoundingBox?.x ?? 0) + xAxisComboboxBoundingBox.width / 2,
140+
(xAxisComboboxBoundingBox?.y ?? 0) + xAxisComboboxBoundingBox.height / 2,
141+
{ steps: 20 }
142+
);
143+
await page.waitForTimeout(250);
144+
}
145+
await xAxisCombobox.click();
146+
147+
const xAxisOption = page.getByRole("option", { name: "文件名稱 (title)" });
148+
const xAxisOptionBoundingBox = await xAxisOption.boundingBox();
149+
if (xAxisOptionBoundingBox) {
150+
await page.mouse.move(
151+
(xAxisOptionBoundingBox?.x ?? 0) + xAxisOptionBoundingBox.width / 2,
152+
(xAxisOptionBoundingBox?.y ?? 0) + xAxisOptionBoundingBox.height / 2,
153+
{ steps: 20 }
154+
);
155+
await page.waitForTimeout(250);
156+
}
157+
await xAxisOption.click();
158+
await page.waitForTimeout(750);
159+
160+
// 點擊「設定篩選條件」按鈕
161+
const filterButton = page.getByRole("button", { name: "設定篩選條件" });
162+
const filterButtonBoundingBox = await filterButton.boundingBox();
163+
if (filterButtonBoundingBox) {
164+
await page.mouse.move(
165+
(filterButtonBoundingBox?.x ?? 0) + filterButtonBoundingBox.width / 2,
166+
(filterButtonBoundingBox?.y ?? 0) + filterButtonBoundingBox.height / 2,
167+
{ steps: 20 }
168+
);
169+
await page.waitForTimeout(750);
170+
}
171+
await filterButton.click();
172+
await page.waitForTimeout(750);
173+
174+
// 選擇篩選條件
175+
const filterCombobox = page
176+
.getByRole("combobox")
177+
.filter({ hasText: "選擇屬性" });
178+
const filterComboboxBoundingBox = await filterCombobox.boundingBox();
179+
if (filterComboboxBoundingBox) {
180+
await page.mouse.move(
181+
(filterComboboxBoundingBox?.x ?? 0) + filterComboboxBoundingBox.width / 2,
182+
(filterComboboxBoundingBox?.y ?? 0) +
183+
filterComboboxBoundingBox.height / 2,
184+
{ steps: 20 }
185+
);
186+
await page.waitForTimeout(250);
187+
}
188+
await filterCombobox.click();
189+
await page.waitForTimeout(500);
190+
191+
const filterOption = page.getByRole("option", {
192+
name: "應用 (multi_select)",
193+
});
194+
const filterOptionBoundingBox = await filterOption.boundingBox();
195+
if (filterOptionBoundingBox) {
196+
await page.mouse.move(
197+
(filterOptionBoundingBox?.x ?? 0) + filterOptionBoundingBox.width / 2,
198+
(filterOptionBoundingBox?.y ?? 0) + filterOptionBoundingBox.height / 2,
199+
{ steps: 20 }
200+
);
201+
await page.waitForTimeout(250);
202+
}
203+
await filterOption.click();
204+
await page.waitForTimeout(500);
205+
206+
// 選擇篩選條件類型
207+
const filterConditionCombobox = page
208+
.getByRole("combobox")
209+
.filter({ hasText: "包含" });
210+
const filterConditionComboboxBoundingBox =
211+
await filterConditionCombobox.boundingBox();
212+
if (filterConditionComboboxBoundingBox) {
213+
await page.mouse.move(
214+
(filterConditionComboboxBoundingBox?.x ?? 0) +
215+
filterConditionComboboxBoundingBox.width / 2,
216+
(filterConditionComboboxBoundingBox?.y ?? 0) +
217+
filterConditionComboboxBoundingBox.height / 2,
218+
{ steps: 20 }
219+
);
220+
await page.waitForTimeout(250);
221+
}
222+
await filterConditionCombobox.click();
223+
await page.waitForTimeout(500);
224+
225+
const conditionOption = page.getByRole("option", {
226+
name: "包含",
227+
exact: true,
228+
});
229+
const conditionOptionBoundingBox = await conditionOption.boundingBox();
230+
if (conditionOptionBoundingBox) {
231+
await page.mouse.move(
232+
(conditionOptionBoundingBox?.x ?? 0) +
233+
conditionOptionBoundingBox.width / 2,
234+
(conditionOptionBoundingBox?.y ?? 0) +
235+
conditionOptionBoundingBox.height / 2,
236+
{ steps: 20 }
237+
);
238+
await page.waitForTimeout(250);
239+
}
240+
await conditionOption.click();
241+
await page.waitForTimeout(500);
242+
243+
// 選擇篩選值
244+
const filterValueCombobox = page.getByRole("button", { name: "×" }).first();
245+
const filterValueComboboxBoundingBox =
246+
await filterValueCombobox.boundingBox();
247+
if (filterValueComboboxBoundingBox) {
248+
await page.mouse.move(
249+
(filterValueComboboxBoundingBox?.x ?? 0) +
250+
filterValueComboboxBoundingBox.width / 2,
251+
(filterValueComboboxBoundingBox?.y ?? 0) +
252+
filterValueComboboxBoundingBox.height / 2,
253+
{ steps: 20 }
254+
);
255+
await page.waitForTimeout(250);
256+
}
257+
await filterValueCombobox.click();
258+
await page.waitForTimeout(500);
259+
260+
const filterValueOption = page.locator("label").filter({ hasText: "前端" });
261+
const filterValueOptionBoundingBox = await filterValueOption.boundingBox();
262+
if (filterValueOptionBoundingBox) {
263+
await page.mouse.move(
264+
(filterValueOptionBoundingBox?.x ?? 0) +
265+
filterValueOptionBoundingBox.width / 2,
266+
(filterValueOptionBoundingBox?.y ?? 0) +
267+
filterValueOptionBoundingBox.height / 2,
268+
{ steps: 20 }
269+
);
270+
await page.waitForTimeout(250);
271+
}
272+
await filterValueOption.click();
273+
await page.waitForTimeout(500);
274+
275+
// 點擊「套用篩選」按鈕
276+
const applyFilterButton = page.getByRole("button", { name: /^.*/ });
277+
const applyFilterButtonBoundingBox = await applyFilterButton.boundingBox();
278+
if (applyFilterButtonBoundingBox) {
279+
await page.mouse.move(
280+
(applyFilterButtonBoundingBox?.x ?? 0) +
281+
applyFilterButtonBoundingBox.width / 2,
282+
(applyFilterButtonBoundingBox?.y ?? 0) +
283+
applyFilterButtonBoundingBox.height / 2,
284+
{ steps: 20 }
285+
);
286+
await page.waitForTimeout(250);
287+
}
288+
await applyFilterButton.click();
289+
await page.waitForTimeout(500);
290+
291+
// 點擊「生成圖表」按鈕
292+
const generateChartButton = page.getByRole("button", { name: "生成圖表" });
293+
const generateChartButtonBoundingBox =
294+
await generateChartButton.boundingBox();
295+
if (generateChartButtonBoundingBox) {
296+
await page.mouse.move(
297+
(generateChartButtonBoundingBox?.x ?? 0) +
298+
generateChartButtonBoundingBox.width / 2,
299+
(generateChartButtonBoundingBox?.y ?? 0) +
300+
generateChartButtonBoundingBox.height / 2,
301+
{ steps: 20 }
302+
);
303+
await page.waitForTimeout(250);
304+
}
305+
await generateChartButton.click();
306+
307+
// 等待錯誤訊息出現
308+
const errorMessage = await page
309+
.locator("div")
310+
.filter({ hasText: "Failed to execute query" })
311+
.nth(3);
312+
await errorMessage.waitFor({ state: "visible" });
313+
const errorMessageBoundingBox = await errorMessage.boundingBox();
314+
if (errorMessageBoundingBox) {
315+
await page.mouse.wheel(errorMessageBoundingBox.x, 200);
316+
await page.mouse.move(
317+
errorMessageBoundingBox.x,
318+
errorMessageBoundingBox.y,
319+
{ steps: 20 }
320+
);
321+
await page.mouse.move(
322+
errorMessageBoundingBox.x + errorMessageBoundingBox.width,
323+
errorMessageBoundingBox.y,
324+
{ steps: 20 }
325+
);
326+
await page.waitForTimeout(250);
327+
}
328+
await page.waitForTimeout(2000);
329+
});

0 commit comments

Comments
 (0)